Skip to content

Roadmap Overview

Decision Value
Assumed baseline Comfortable with Java. New to Spring.
Language / runtime Java 21 LTS, virtual threads enabled
Framework Spring Boot 3.5.x (Jakarta EE 10 namespace)
Domain Event ticketing marketplace
Architecture Modular monolith → extract 1 service (step 20)
Code structure Layered → hexagonal refactor (step 9) → lightweight CQRS read path (step 11)
Infrastructure PostgreSQL 16, Redis 7, RabbitMQ 3, Testcontainers
Admin frontend Thymeleaf + HTMX (step 5)
Customer frontend React 19 + TypeScript + Vite (steps 15–18)
Auth progression Session+CSRF → hand-rolled JWT → Spring Authorization Server
Testing Tests gate every step. Test-first on concurrency, money, state transitions.
Ops Docker Compose + nginx LB (2 app instances) → single VPS. K8s conceptual only.
Admin UI Tailwind 4 + daisyUI 5 + custom theme + ApexCharts. No template adopted wholesale.
Storefront UI Tailwind 4 + shadcn/ui, custom design tokens
Java quality Spotless + Palantir format, Error Prone, NullAway, ArchUnit, SpotBugs + FindSecBugs, OWASP dependency-check. No Checkstyle, no PMD — see step 0.
Frontend quality Biome (lint + format + imports) + minimal ESLint for react-hooks + TypeScript strict
AI harness AGENTS.md from step 0, evolving v1→v5. Two-mode contract per step.

Note on step count: the roadmap is 26 numbered steps (0 through 24, plus 5b). Step 20 (service extraction + deploy) has its own slot rather than being folded into the resilience step.


Ticketing was chosen because it forces the advanced topics rather than just decorating with them:

  • Overselling is a real correctness problem → locking, seat holds with TTL, distributed locks
  • Payment gateways are asynchronous and unreliable → idempotency keys, outbox pattern, webhooks, compensation
  • On-sale moments are genuine traffic spikes → caching, rate limiting, read/write split, horizontal scaling
  • Seat maps demand real client state → the React path isn’t a toy

A CRUD app cannot teach these. Any caching or locking you added to one would be for show.


Every step is written against the same three events. Seed them at step 6 and keep them for the rest of the roadmap — they disagree with each other on purpose, and a feature that works for all three is a feature that works.

Event Setup The rule it exists to break
Hamlet at the Lyric Theatre 380 numbered seats across stalls, circle and balcony; three price tiers; twelve showtimes on one venue Availability is per showtime, not per event. A catalog page listing all twelve dates is where the N+1 shows up.
Riverside Arena, one night 5,000 seats, a single showtime, on sale 09:00 Friday Everyone arrives in the same minute: bulk generation, contention on the last rows, cache stampede, rate limiting, two instances
The Foundry, club gig 400 standing places, general admission, no seat map A ticket with no seat. Quota on a TicketType instead of Seat rows — the case a seat-only model cannot express.

Concrete questions to keep asking as you build: does the catalog page for Hamlet issue twelve queries or one? When the arena’s last ten seats go, do exactly ten buyers win? Can The Foundry sell its 400th ticket without a single Seat row existing?


Organizer ──creates──> Event ──has──> Showtime ──at──> Venue
│ │
│ └──has──> SeatMap ──has──> Seat
│
└──has──> TicketType (price, quota)
Customer ──creates──> Order ──holds──> SeatHold (TTL) ──becomes──> Ticket
│
└──has──> Payment ──driven by──> PaymentEvent (webhook)

Module boundaries (package-enforced from step 1, ArchUnit-enforced from step 9):

com.ticketflow
├── catalog/ events, showtimes, venues, seat maps
├── ordering/ orders, holds, tickets, the purchase transaction
├── payments/ gateway adapters, webhooks, reconciliation
├── identity/ users, roles, auth
├── notification/ email/SMS dispatch (extracted to own service at step 20)
└── shared/ cross-cutting: errors, config, correlation, base types

These are continuous, not per-step. Each switches on at a specific step and stays on. CI fails on violation.

Guardrail On from Rule
Spotless + Palantir format Step 0 Build fails on unformatted code. Auto-fixable.
Error Prone + NullAway Step 0 Compile fails on likely-NPE paths, equals type mismatches, format-string errors, unclosed resources. Highest-value tool here; most commonly skipped.
Lefthook pre-commit Step 0 Format + fast checks before commit. Must run in <5s or it gets bypassed.
make verify Step 0 One command: format → compile → test → arch → frontend lint. Local, CI, and agent loop all use it.
GitHub Actions CI Step 3 Runs make verify on every push. Nothing else.
Testcontainers in CI Step 3 Integration tests run real Postgres. No H2 — H2 behaves differently on locking.
JaCoCo coverage gate Step 6 ≥80% on **/domain/** only. Not global; a global coverage target produces fake tests.
ArchUnit: layer rules Step 9 Controllers must not reference repositories. Domain must not import Spring.
ArchUnit: module rules Step 9 Modules communicate only via published interfaces or domain events
OpenAPI contract diff Step 12 Breaking API change fails CI unless version bumped
Biome + TS strict Step 15 Frontend lint, format, import order. Type errors fail the build.
SpotBugs + FindSecBugs Step 14 Injection paths, weak crypto, XSS sinks. Build fails on high-confidence findings.
Dependency vulnerability scan Step 14 OWASP dependency-check; build fails on CVSS ≥7
Bundle size budget Step 18 Frontend CI fails if the budget is exceeded

On what’s deliberately absent: Checkstyle and PMD. Checkstyle mostly enforces formatting, which Spotless already does deterministically and with auto-fix. PMD’s genuinely useful findings overlap Error Prone with a far worse signal-to-noise ratio. Running all four produces hundreds of warnings nobody reads, and a gate that gets ignored is worse than no gate, because it teaches you that build output is noise. If you later want an aggregate dashboard, SonarQube Community at step 20 is optional. Not before.


Each step’s structure, the LEARN/BUILD mode contract, and the five verification layers are explained in Getting Started. Read that first if you haven’t.