One flash-sale ticketing system, implemented in seven backend frameworks, served by a single frontend that has no idea which backend is answering. It is a teaching lab, not a product. The goal is to show the same hard problems being solved seven different ways, and to explain every choice well enough that you could make it yourself.
The domain is a concert going on sale: far more people than tickets, all arriving at the same second. That one scenario forces everything worth teaching. Stock contention, distributed locks, idempotency, a virtual queue, asynchronous payment, circuit breakers, and a load test whose entire job is to try to make you oversell.
Three rules the whole repo is built to keep true:
- One contract.
contract/openapi.yamlis the single source of truth. Every backend implements it exactly. See ADR 0002. - The frontend is blind to the backend. It talks to one gateway. Swapping the
active backend is a routing change, never a code change. There is no
if framework == ...anywhere, and there never will be. - Frameworks only do delivery. Business rules live in a framework-agnostic service layer. See ADR 0003.
- The contract — one OpenAPI spec (
contract/openapi.yaml) that every backend implements and the frontend generates its client from, plus a shared Postgres schema and a contract test suite that runs against any backend. - Seven backends, each a full hexagonal implementation of the contract, each passing the same 16 contract tests with zero changes to the suite or the frontend: Go, FastAPI, NestJS, Express, Laravel, Symfony, Phalcon. Idempotent reservations with a distributed lock and atomic stock decrement, TTL holds with a sweeper, async payment via RabbitMQ, signed webhooks, JWT with refresh rotation.
- A frontend (React + Vite + TS) with a typed client generated from the contract, TanStack Query caching, route-level code splitting, in-memory tokens with refresh rotation, and the full flow — events → waiting room → reserve with countdown → checkout → order polling — verified end-to-end in a browser.
- Resilience — circuit breaker + retry/backoff + timeout on the payment path, driven
live by the fake gateway's runtime failure switch (graceful degradation: orders stay
pending, no 5xx). - Observability — Prometheus + Grafana RED dashboard (edge metrics, works for every
backend), OpenTelemetry tracing → Tempo (a reservation traces as
POST→redis.lock.acquire→db.decrement_inventory), Loki + Promtail logs. - Scale — a k6 stampede (
make load) proving no overselling (100 of 100 seats under 400 concurrent attempts, 0 over-sold, 0 5xx) on multiple backends and across 3 replicas sharing one Postgres + Redis; Kubernetes manifests (Deployment, Service, HPA, probes, ConfigMap/Secret, Ingress) ininfra/k8s. - Security — gateway↔backend mutual TLS (worked example, client cert required and verified), least-privilege DB role, signed webhooks, rate limiting, strict input validation, error envelopes that leak nothing. See the security-layers recipe.
- The recipes — concept write-ups in docs/recipes (backend and client), each pointing at real code, plus a README per backend and the ADRs behind the key decisions.
The same ticketing client, built three times — Kotlin Multiplatform, Flutter, React Native — so you can compare how each platform solves the same problems. Same contract, same seven-screen flow, same states, each app blind to the backend behind a single base URL. It is the mobile companion to the seven backends above.
All three are built, verified and offline-first, consuming the same OpenAPI contract:
- KMP — Compose Multiplatform, shared UI in
commonMain, multiplatform ViewModels. iOS and Android compile; a unit suite runs on the Android host; the Android APK builds. - Flutter — Cubits +
dio.analyzeclean; unit + widget tests; web bundle builds. - React Native — Expo, zustand + TanStack Query +
ky.typecheckclean; unit tested.
Each app checks server reachability with a bounded probe (so there is no infinite loading), stays usable offline, handles auth with refresh-token rotation and global sign-out, and reads the base URL from one place — pointed at an external tunnel URL for device testing, never a local IP.
- apps/README.md — the three clients, how to run each, and where to set the endpoint.
- shared/README.md — what is single-sourced and why.
- docs/client-architecture.md — the shared layering.
- docs/client-state-machines.md — reservation + order, in Mermaid.
- ADR 0007 / ADR 0008 — the decisions behind it.
Requires Docker and Docker Compose.
make up # copies .env, builds, starts the shared infrastructure
make ps # see what is running
make down # stop it
make clean # stop it and wipe local dataAfter make up you get Postgres (migrated and seeded), Redis, RabbitMQ, the fake
payment gateway, the Traefik gateway, and the Go reference backend behind it. The API
is live at https://localhost/api (self-signed TLS, so use curl -k).
- Web app (the SPA): http://localhost/
- API (via gateway): https://localhost/api/events (also http://localhost/api for local dev)
- RabbitMQ management UI: http://localhost:15672
- Traefik dashboard: http://localhost:8081
- Fake payment gateway health: http://localhost:9090/health
Demo credentials (seeded): buyer@ticketing.local / password123.
To test from another device (a real phone, an emulator), expose the gateway over an external
HTTPS URL instead of a local IP: make tunnel (ngrok) or cloudflared tunnel --url http://localhost:80, then point the web and mobile clients at https://<tunnel-host>/api. See
the tunnel recipe.
Switch the active backend by editing one line in .env — COMPOSE_PROFILES is one of
go, fastapi, nest, express, laravel, symfony, phalcon — and running
make up again. The frontend, gateway, and contract do not change. See
ADR 0006.
# log in, then list events
curl -sk -XPOST https://localhost/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"buyer@ticketing.local","password":"password123"}'For an item-by-item audit of the spec's Definition of Done and the security / performance / availability / concurrency checklists against the code — proven, partial, or scoped-future — see docs/status.md.
contract/ openapi.yaml (source of truth), db schema + migrations, contract tests
backends/ one directory per framework: go, fastapi, nest, express, laravel, symfony, phalcon
services/ payment-gateway-fake (external provider simulator with a failure switch)
frontend/ React + Vite + TypeScript SPA
shared/ single-sourced client assets: contract mirror, tokens, scenarios, copy
apps/ the three mobile clients: kmp, flutter, react-native
infra/ gateway config, k8s manifests, observability, load tests
docs/ architecture.md, domain-model.md, client-architecture.md, adr/, recipes/
- docs/architecture.md — how the pieces fit together.
- docs/domain-model.md — entities, invariants, and the reservation/order state machines.
- docs/adr/ — why things are the way they are.
- contract/openapi.yaml — the contract everything obeys.
This lab is a workout, not a product, and it isn't here to show off. Before moving to Sweden I worked as a mobile developer — native Android and iOS — and also fullstack (PHP, Node) and devops/SRE, so it exists to keep those muscles honest: to try languages I don't reach for daily (Go), sharpen my Python, and actually learn Flutter and React Native rather than nod along in meetings.
I picked a problem of medium complexity on purpose. A flash sale makes you get correctness right from the database up and then defend it through the API, the gateway, and every client, so one thread — don't oversell — runs the whole length of the stack: concurrency, security, and how a web app and three mobile clients consume the exact same contract. I've never held a grudge against a language, framework or platform. (Okay: ASP.)
I learned a great deal building it and I'm still learning. If any of it is useful to you, that's the point.
Jackson Mafra — linkedin.com/in/jacksonfdam
MIT. See the license field in the contract.