Project Deep-Dive · Philip Olaomo · 2026

If they ask
about my projects.

The hardest questions an interviewer can pull straight off my CV — and the answers, written from inside the actual code. One section per pinned project.

PROJECT 01

Helfa (Immigration Helper)

Spring Boot 3 + React 19 platform refactoring a visa-CRM into a guidance app for international residents in Germany.

Q1. Walk me through the visa-application state machine. Why an explicit machine instead of just a status column?

The lifecycle is DRAFT → SUBMITTED → APPROVED | REJECTED. I picked an explicit machine for three reasons. First, illegal transitions are a class of bug I’d rather not have — without a machine, nothing stops a controller from moving an APPROVED visa back to DRAFT. Second, every transition writes a row to visa_status_history in the same transaction, so the audit trail is automatic. Third, the controller surface is forced to be transition-shaped — POST /visas/{id}/submit, not a generic PATCH on status — which keeps the API resistant to client-side abuse.

Q2. What does the audit log buy you that just updated_at wouldn’t?

Three things. Why a transition happened (a column for actor, reason). The chain of transitions, not just the latest. Trust — the user can read the timeline themselves. For a population that’s already anxious about bureaucracy, “here’s every move on your case, with timestamps” is the feature that builds trust faster than UX polish does.

Q3. You’re refactoring it into Helfa under a feature flag — what was wrong with the original CRM model?

The CRM modelled the state of the application. Helfa models the state of the person — what tasks remain across all of their bureaucracy, sequenced by priority and deadline. The visa app is one of those tasks. The CRM was correct, just at the wrong level of abstraction for the user.

Q4. How is the migration staged?

Five phases gated behind features.guidance.enabled. Phase 1 introduces the new domain (Task, Journey, Plan) alongside the old. Phase 2 backfills data. Phase 3 swaps the read path. Phase 4 swaps the write path. Phase 5 retires the old endpoints. Tag pre-migration-baseline marks the last commit before any of this. The legacy CRM stays live until Phase 4 cutover, so we can roll back at any phase.

Q5. Authentication is JWT. Why JWT and not server-side sessions?

Helfa’s frontend is a React SPA and there’s a planned mobile app — two clients, no shared origin, no easy session-cookie story. JWT plus a refresh token gives me a stateless API that scales horizontally without a shared session store. Trade-off acknowledged: revocation before expiry is harder. I mitigate with short access-token TTL (15m) and a refresh-token rotation policy.

Q6. The Ausländerbehörde directory has 24 real offices — where does that data come from, and how do you keep it fresh?

It started as hand-curated data scraped from federal and municipal sites with a Python script. “Fresh” is the unsolved problem — the federal data isn’t a feed. The plan is a periodic re-scrape with diff alerts to a maintainer, so a human reviews changes before they go live. Stale phone numbers in this domain are worse than no phone numbers.

Q7. Stripe is in the backend but not on the frontend — why?

Honesty: scope. The premium-subscription flow needs commerce flows, billing-portal, dunning, tax handling — all real product work. I scaffolded the backend webhook handler and entity model so Phase 4 of the Helfa migration can wire the frontend without a schema change. Building a half-broken paywall would be worse than no paywall.


PROJECT 02

Realworld DSAR Workflow

Spring Boot service + React/Vite operator console for GDPR data-subject-access-request lifecycle.

Q1. What’s a DSAR and why is it interesting from an engineering standpoint?

A Data Subject Access Request is the right under GDPR to ask an organisation what personal data they hold on you. The engineering interest is that it forces the system to know its own data — which tables hold PII, which fields, who’s responsible for which subsystem, what the retention rules are. It’s a practical audit of the architecture.

Q2. Show me a status transition that fails. What does the API do?

Trying to move an already-resolved request to IN_PROGRESS: the service throws InvalidStatusTransitionException. The @RestControllerAdvice translates it into a 409 Conflict with an RFC 7807 ProblemDetail body that names the current state and the attempted target. The integration test exercises exactly that path so a regression is caught at PR time.

Q3. Resolution-notes are auto-generated on certain transitions. Why?

Operator hygiene. If a request is closed, there must be a reason on file — for the regulator, for the next operator, for the requester themselves. Auto-generating a structured note (“Closed by <actor> with outcome <X>”) on the transition guarantees the note exists. Operators can still add a free-text addendum.

Q4. Tests — what do you cover?

End-to-end lifecycle (create → in-progress → resolved), validation failures (missing fields, invalid enum values), conflict handling (illegal transitions, concurrent edits), note CRUD with permissions, and the summary-by-status report. Anything that can be reached from an HTTP client has at least one integration test.

Q5. Why Gradle? Why not Maven?

The Spring Boot starter still defaults to Maven, but Gradle’s incremental builds and Kotlin DSL are nicer at this scale. The team-side cost — “we know Maven” — is the only real reason to pick Maven; for a solo project the choice is taste.

Q6. Explain the queue-filtering UI.

The UI hits GET /requests?status=...&assignee=...&dueWithinDays=... with debounced query updates. Pagination is keyset on (createdAt, id) so deep scrolling stays O(log n). Inline actions (assign, escalate, resolve) optimistically update the cache via React Query’s setQueryData, then reconcile against the server response.


PROJECT 03

Nova Schilda Drone Network

Group project (3 contributors) modelling a fictional city as a graph; five planning features built on BFS, Dijkstra, max-flow, bridges, and facility location.

Q1. Why model the city as a graph in the first place?

Because the problems we wanted to answer — “can drone X reach district Y?”, “cheapest route?”, “maximum throughput?”, “single points of failure?”, “where to put charging stations?” — are all classical graph problems. The graph isn’t a modelling choice; the choice was whether to use a sparse adjacency-list representation or a matrix. We benchmarked both and the list won by >5× on the bigger inputs.

Q2. How do edge weights map to the real domain?

Each edge has two attributes: an energy cost (Wh, used for shortest-path) and a capacity (deliveries/hour, used for max-flow). Same graph, two algorithms reading different attributes. That separation kept the code clean — no algorithm had to know about both.

Q3. Walk me through F3 — delivery capacity via max-flow.

We used Edmonds-Karp (BFS-based augmenting paths) for predictable O(V·E²) behaviour. Source = origin hub, sink = destination, edges weighted by capacity, all flows in deliveries/hour. The output is the integer max flow plus the min cut — which is the operationally interesting answer (“these three corridors are your bottleneck”).

Q4. F4 — bridge detection. What does “resilience” mean here?

A bridge is an edge whose removal disconnects the graph. In the drone domain, that’s a corridor whose failure splits the network into two unreachable regions. We used Tarjan’s algorithm — a single DFS tracking discovery times and the lowest reachable ancestor — for O(V+E). The output drives a redundancy-priority list: “these are the corridors you absolutely need a backup for.”

Q5. F5 — charging-station placement. This is the hardest feature; how did you tackle it?

It’s a facility-location problem. Pure formulations are NP-hard, so we used a greedy approximation: pick the candidate site that maximises coverage (number of delivery points within a max-energy distance), update remaining demand, repeat until budget exhausted. We documented the bound — greedy gives a (1 - 1/e)-approximation when the function is submodular, which it is here — so the report doesn’t over-claim optimality.

Q6. How did the team divide the work?

Three of us, five features. We agreed on the Graph + Loader interfaces first (one PR, the whole team), then I owned F1, F2 and F4; one teammate owned F3; the other owned F5. Reports and visualisation were pair-written. The discipline was: nobody touches features/ except their owner, but everybody can refactor the shared graph package — gated by code review.


PROJECT 04

Safari Paths

2D Godot game exploring AI-driven creature movement and tile-based pathfinding as gameplay.

Q1. Why Godot and not Unity?

Open source, lightweight, built-in scene tree maps cleanly to a node-based mental model, and GDScript’s ergonomics for prototypes are unbeaten. Unity is a better choice if I needed multiplayer or platform parity at scale, but for a 2D pathfinding sandbox Godot ships you faster.

Q2. Walk me through the pathfinding.

Tile-based A*. Each tile has a terrain type with a movement cost (grass 1.0, sand 1.5, water impassable). The heuristic is Manhattan distance scaled by the cheapest terrain. The interesting choice was making the cost function depend on the creature type — predators ignore water-edge cost, herbivores prefer the safer terrain — so different creatures take different paths over the same grid.

Q3. How do encounters work?

A weighted random spawn keyed by tile-biome. Each biome has a spawn table; on entry the game rolls against it. Cooldowns are per-tile so the player can’t farm a single hot spot. Encounters trigger a state transition on the player controller — same input system, different action set during combat.

Q4. Input system — what made it tricky?

Two control schemes (keyboard + gamepad) sharing one action map via Godot’s InputMap, plus dynamic remapping. The hard part wasn’t binding — it was making sure the on-screen prompts reflected the actual binding (“press A” vs “press Space”) without manual maintenance. I wrote a small system that introspects the InputMap and renders the correct glyph.


PROJECT 05

Talk-to-Anybody (SpeakCoach)

Cross-platform AI public-speaking coach: Expo React Native + Fastify + shared Zod schemas.

Q1. Why a monorepo? Wasn’t two repos easier?

The shared package (packages/shared) holds Zod schemas and TypeScript types that are the contract between mobile and API. With two repos that contract drifts every Tuesday and you spend Wednesday debugging type mismatches. With a monorepo the same TypeScript types are imported directly; a breaking change shows up as a compile error in the consumer at PR time.

Q2. Why Fastify and not Express?

Fastify is faster, has first-class TypeScript and JSON-schema support, and validates requests at the handler boundary using the same schemas the client uses. Express works, but it’s leaving performance and type-safety on the table for an ecosystem advantage I don’t need.

Q3. What does “stage-gated” mean for the build plan?

Each stage is a fully-shippable slice. Stage 1: scaffold both apps. Stage 2: recording end-to-end (record → upload → store). Stage 3: analysis pipeline. Stage 4: purchases. Stage 5: dashboards. The rule is no stage merges until it ships its own happy path. That’s a direct lesson from a 2,000-line PR I once let rot for two weeks — never again.

Q4. Recording in Expo — what’s the gotcha?

Permissions on iOS aren’t automatic — you have to declare NSMicrophoneUsageDescription in app.json. Then there’s the foreground/background story: a recording started in foreground stops when the app backgrounds unless you explicitly request a background-audio capability. Both of those will bite you if you don’t test on a real device.


PROJECT 06

Fit Tracker

React + Spring Boot fitness tracker; my full-stack starter project.

Q1. What did you actually build?

Workout, exercise, and set logging persisted via JPA. Per-exercise history with progress charts. JWT-secured REST API. A React UI with charts. The point was end-to-end ownership of a thin slice — not a perfect product, but the full vertical from controller to chart.

Q2. What would you do differently if you started today?

Three things. (1) Use TypeScript on the frontend instead of plain JS — the type errors would have caught half my early API mismatches. (2) Add Flyway from day one instead of relying on Hibernate’s schema-update — that habit cost me a clean migration story. (3) Write integration tests with Testcontainers from the first commit, not bolted on later.

Q3. The data model — workouts, exercises, sets — how did you split it?

Three entities. Workout(id, user, date, notes). Exercise(id, name, muscle_group, ...) — a catalogue. Set(id, workout_id, exercise_id, reps, weight, order) — the actual log row. The decision I’d revisit: I kept exercises global instead of per-user. Custom exercises are a real user need and retro-fitting them means a schema migration.

Q4. Charts — how did you implement them?

Recharts on the frontend, fed by a per-exercise history endpoint that returns (date, max_weight, total_volume). Server-side aggregation, not client-side — the moment you have 12 months of data, doing it in React is sluggish. Rule I follow now: if the chart needs more than a row-level transform, the API does it.


Built by Philip Olaomo · 2026 · github.com/lastiroko · philipoluwalani@gmail.com