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.
Spring Boot 3 + React 19 platform refactoring a visa-CRM into a guidance app for international residents in Germany.
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.
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.
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.
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.
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.
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.
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.
Spring Boot service + React/Vite operator console for GDPR data-subject-access-request lifecycle.
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.
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.
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.
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.
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.
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.
Group project (3 contributors) modelling a fictional city as a graph; five planning features built on BFS, Dijkstra, max-flow, bridges, and facility location.
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.
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.
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”).
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.”
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.
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.
2D Godot game exploring AI-driven creature movement and tile-based pathfinding as gameplay.
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.
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.
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.
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.
Cross-platform AI public-speaking coach: Expo React Native + Fastify + shared Zod schemas.
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.
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.
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.
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.
React + Spring Boot fitness tracker; my full-stack starter project.
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.
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.
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.
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.