A backend ticket-sales service for small events — designed, decided, coded and tested end-to-end by the forge pipeline, from a single paragraph of product idea. A human only reviewed and approved decisions.
The whole system was grown from a plain-language brief — no schemas, no API spec, no diagrams. forge extracted the domain, surfaced the decisions, and built the code.
— docs/idea.md, the only human-authored spec
Every stage left a durable, inspectable artifact under meta/. The trail
is part of the deliverable — you can audit how each line of code traces back to intent.
| Stage | What forge produced | Artifact |
|---|---|---|
| Brainstorm → domain | vision, user stories, formal domain model (events, commands, aggregates, policies, glossary) | inputs/ · domain/ |
| Capabilities & contexts | business capabilities, bounded-context map | domain/ |
| Decisions | 19 ADRs in Nygard format, each with rejected alternatives | decisions/adr/ |
| Synthesis | C4 diagrams + a requirement→component→test trace matrix | c4/ · trace.yml |
| Kanban | 12 cards across waves, each built in its own git worktree, with review cycles | kanban/ |
| Docs | layered "how it works" + OpenAPI contract | docs/ |
One Go deployable; contexts isolated by package and by Postgres schema; cross-context communication only through a transactional outbox. Each choice is pinned to an ADR — and the code follows it.
Events, reservations, the capacity guard, payment intake, hold expiry, reopen-on-release.
Orders, tickets, the decoupled ticket-email worker.
Cancellation, refund requests with retry & escalation.
Outbox relay, scheduler sweep, append-only audit log, injectable UTC clock.
Anti-corruption layers to providers · dedicated idempotency store · ports & adapters.
Schema-per-context (ADR-0003) · async edges via outbox (ADR-0004) · single clock (ADR-0019).
The single most important requirement is enforced by one guarded atomic UPDATE —
the only write-path that can increment held_seats. No application-level read-modify-write,
no retry loop.
UPDATE inventory.events SET held_seats = held_seats + $3 WHERE id = $1 AND status = 'published' AND sales_open_at <= $2 AND sales_close_at > $2 AND held_seats + sold_seats + $3 <= capacity -- 0 rows affected ⇒ rejected. Oversell -- is impossible by construction (NFR-002).
200 buyers race for 100 seats → exactly 100 succeed,
held_seats == 100, and the reservation rows are reconciled against the successes — no phantom inserts.
The never-oversell & per-buyer proofs run behind an integration build tag
against a live database — properties that cannot be faked against a mock.
The refund path is tested through transient failures, persistent failures, lease re-claims and escalation — not just the happy case.
A full reserve → pay → confirm → order flow, plus the late-callback auto-refund and the cancellation→refund→release chain.
The brief hides a trap: the 10-minute hold is shorter than the maximum 15-minute callback delay. A payment can confirm after the seats were released.
forge didn't silently pick one. It surfaced the tension as a decision, then resolved it in code: a late callback re-checks availability and either fulfils or auto-refunds — the buyer is never charged for a ticket that can't be delivered.
"The 10-minute hold window is shorter than the maximum 15-minute callback delay…"
Resolved via the late-callback reconciliation policy (ADR-0007 auto-refund) and implemented in a dedicated card with concurrency tests.
— an independent audit from a separate source
holdSeatsSQL with its transaction-order & TOCTOU note is an exemplary fragment."The audit's two most serious findings weren't sloppiness in the code — they were organic drift between model and code: exactly the failure class the framework exists to catch.
ADR-0015 says the 4-ticket cap counts held + confirmed. The code counted only
held — a card was closed with a "finish later" comment, yet the trace marked the
requirement covered. A buyer with 4 paid tickets could hold 4 more.
7 of 53 referenced tests no longer existed under those names (renamed in the email-worker refactor; coverage existed, links were dead), and 2 of 26 code-globs pointed at directories that were never used.
| Audit finding | Resolution | Status | Commit |
|---|---|---|---|
| INV-003 counts only held seats | Count held + paid; honest tests that fail on the old query | FIXED | e567a3e |
| Trace matrix stale (7 names, 2 globs) | Re-synced to real test names & inventory/·refunds/ | FIXED | a3f62ec |
| Validators let both drifts through | Closed two blind spots: self-referencing test check & dead glob matcher; added a spec↔test↔code review lens | FIXED | forge a92b1f4 · 2ab938e |
| No CI to fail the build on drift | GitHub Actions drift-gate workflow | FIXED | 18879dd |
| One-line README, no one-command run | Full README + generated Makefile & docker-compose (make run) | FIXED | 81cc903 · e362f3c |
| Auth is a header stub; webhook unsigned | Explicit "demonstration · no security hardening" disclaimer in README | NOTED | 81cc903 |
Findings ①–② aren't a defect to quietly sweep away — they're the most convincing content the project could have produced: real drift, caught and fixed by the very framework built to catch it.
Two genuine "where AI errs even inside the pipeline" cases beat any hypothetical: "closed a card with a 'do it later' comment and marked the requirement covered" and "didn't re-sync the trace after a refactor."
Turnstile is a teaching example: a real backend whose every decision, requirement, test and even its mistakes are inspectable end to end. That transparency — including the drift it caught in itself — is the whole point.
docs/idea.md — the brief
🧭 meta/architecture/ — model & ADRs
🗺 meta/architecture/trace.yml — traceability
📓 meta/kanban/retro.md — the build retro