Skip to content

jacksonfdam/TicketingLabs

Repository files navigation

Ticketing Labs

CI

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.

The premise

Three rules the whole repo is built to keep true:

  1. One contract. contract/openapi.yaml is the single source of truth. Every backend implements it exactly. See ADR 0002.
  2. 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.
  3. Frameworks only do delivery. Business rules live in a framework-agnostic service layer. See ADR 0003.

What's built

  • 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 POSTredis.lock.acquiredb.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) in infra/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 client lab

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. analyze clean; unit + widget tests; web bundle builds.
  • React Native — Expo, zustand + TanStack Query + ky. typecheck clean; 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.

Quick start

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 data

After 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).

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 .envCOMPOSE_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.

Repository layout

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/

Where to read next

Why I built this

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

License

MIT. See the license field in the contract.

About

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.

Resources

Stars

Watchers

Forks

Contributors

Languages