Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 72 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,102 @@
# Ticket Engine

A high-concurrency ticket booking backend built with Go, Redis, and MySQL.

## Table of Contents
- [Why this project](#why-this-project)
- [Current architecture](#current-architecture)
- [Key capabilities](#key-capabilities)
- [API surface](#api-surface)
- [Quick start (Docker)](#quick-start-docker)
- [Local development](#local-development)
- [Configuration](#configuration)
- [Testing and verification](#testing-and-verification)
- [Load testing and reconciliation](#load-testing-and-reconciliation)
- [V2 roadmap and docs](#v2-roadmap-and-docs)

## Why this project
Traditional databases struggle when very large numbers of users attempt to book at the same time. Ticket Engine moves allocation pressure to Redis, keeps booking allocation atomic, and persists booking state asynchronously to MySQL.

## Current architecture
![Architecture](./architecture.png)
High-concurrency **ticket booking** backend in **Go**, with **Redis** for atomic allocation and **MySQL** as the system of record.

Flow (current implementation):
1. Client sends `POST /book` with `X-User-Id`.
2. Redis Lua allocation is used for atomic allocation + idempotent replay behavior.
3. Booking events are written to a Redis Stream and consumed by a worker.
4. Worker persists sold ticket ownership to MySQL.
## Overview

This design gives fast allocation while preserving eventual consistency in the system of record.
When many users hit `/book` at once, classic row-level booking patterns contend hard. Ticket Engine keeps inventory in a Redis list, allocates with a **Lua script** (atomic + per-user idempotent), **XADD**s booking events to a Redis Stream, and a consumer persists sold ownership to MySQL.

## Key capabilities
- **Atomic booking allocation** in Redis to prevent duplicate ticket assignment.
- **Idempotent user booking behavior** so repeated requests return consistent outcomes.
- **Asynchronous persistence** using Redis Streams + consumer processing.
- **Operational endpoints** for health, readiness, counters, and ticket inventory visibility.
- **Explicit inventory seeding** via command (`go run . seed`) instead of startup auto-minting.
## Architecture

## API surface
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/book` | Attempt booking for one user (`X-User-Id` required) |
| `GET` | `/tickets/count` | Remaining tickets in Redis queue |
| `GET` | `/metrics` | Request/success/failure counters |
| `GET` | `/healthz` | Liveness probe |
| `GET` | `/readyz` | Readiness probe (Redis + MySQL + consumer checks) |
![Architecture](./architecture.png)

Example booking request:
1. Client `POST /book` with `X-User-Id` (optional `X-API-Key` if configured)
2. Redis Lua: `HGET` prior booking → or `LPOP` + `HSET` + `XADD`
3. Stream consumer writes sold state to MySQL
4. Replay path re-enqueues persist if Redis says booked but MySQL lags

## Tech stack

| Area | Tech |
|------|------|
| API | Go 1.22, `net/http` |
| Allocation | Redis 7, embedded Lua (`internal/booking`) |
| Persist | Redis Streams consumer (`internal/persist`), MySQL 8 via GORM |
| Ops | Docker Compose (mysql, redis, seed, server), Makefile test targets |
| Load | k6 (`test.js`), legacy wrk Lua (`test.lua`) |
| UI | React + Vite dashboards under `frontend/` (Dashboard, Login, Queue Simulation) |

## Key engineering decisions

- **Redis owns hot allocation**; MySQL owns durable sold state (eventual consistency by design)
- **Idempotent user booking**: same `user_id` retries return the same `ticket_id` with HTTP 200
- **Explicit seed**: `go run . seed` mints inventory (default 15,000); server start does not auto-mint
- **Race-aware metrics**: counters protected for concurrent `/metrics` under load
- **V2 phased roadmap** documented under `docs/` — phases 1–2 complete; later phases (full recovery/observability/security/CI bench gate) tracked in `docs/V2_PROGRESS.md`

## Features

- Atomic booking allocation (no double-assign of the same ticket id under concurrent clients — covered by unit/integration tests)
- Idempotent replay behavior
- Async MySQL persistence via stream consumer
- Health / readiness / inventory count / metrics endpoints
- Docker Compose local stack
- k6 staged load profile (ramp toward multi-thousand concurrent VUs — **results not yet recorded** in `docs/BENCHMARKS.md`)

## API

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/book` | Book one ticket for `X-User-Id` |
| GET | `/tickets/count` | Remaining Redis queue length |
| GET | `/metrics` | Request / success / failure counters |
| GET | `/healthz` | Liveness |
| GET | `/readyz` | Readiness (Redis + MySQL + consumer checks) |

```bash
curl -X POST http://localhost:8080/book \
-H "X-User-Id: 101"
curl -X POST http://localhost:8080/book -H "X-User-Id: 101"
```

## Quick start (Docker)

```bash
docker compose up
```
## Performance / results

This starts:
- `mysql` (MySQL 8)
- `redis` (Redis 7)
- one-shot `seed` service (`go run . seed`)
- `server` service (`go run .`)
**No published throughput or latency numbers yet.** `docs/BENCHMARKS.md` is a measurement plan only — append runs with git SHA when executed. The repo includes k6 scripts and correctness checks (unique ticket↔user mapping, inventory conservation) to support that work.

Default API endpoint: `http://localhost:8080`
Do **not** treat the old GitHub description claim of “100k requests” as a verified result.

## Local development
If you already run MySQL and Redis locally:
## Getting started

```bash
go run . seed # run once on empty DB
go run . # start API server
docker compose up
# API: http://localhost:8080
```

Useful command:
Local (MySQL + Redis already running):

```bash
go run . reconcile
go run . seed
go run .
go run . reconcile # optional
```

## Configuration
Environment variables used by runtime and compose setup:

| Variable | Purpose |
|---|---|
| `HTTP_PORT` | HTTP server port (default `8080`) |
| `MYSQL_HOST` | MySQL host:port |
| `MYSQL_USER` | MySQL username |
| `MYSQL_PASSWORD` | MySQL password |
| `MYSQL_DATABASE` | MySQL database name |
| `REDIS_HOST` | Redis host:port |
| `TICKET_API_KEY` | Optional API key for protected booking access |
| `TICKET_CORS_ORIGIN` | Allowed CORS origin (default `*`) |

## Testing and verification
## Testing

```bash
make test
make test-race
make test-integration # requires MySQL + Redis
make test-integration # needs MySQL + Redis
```

## Load testing and reconciliation
- k6 script: [`test.js`](test.js)
- post-run SQL reconcile script: [`scripts/post-k6-reconcile.sql`](scripts/post-k6-reconcile.sql)
- legacy V1 script: [`test.lua`](test.lua)
## Project structure

```text
internal/booking/ Lua allocator
internal/persist/ stream consumer
migrations/ SQL schema
docs/ V2 spec, progress, failure matrix, benchmark plan
frontend/ React/Vite UI experiments
test.js k6 load script
```

## V2 roadmap and docs
- V2 target spec: [`docs/V2_SPEC.md`](docs/V2_SPEC.md)
- V2 phase progress: [`docs/V2_PROGRESS.md`](docs/V2_PROGRESS.md)
- failure/recovery matrix: [`docs/FAILURE_MATRIX.md`](docs/FAILURE_MATRIX.md)
- benchmark plan: [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md)
## Future improvements

Current phase status is tracked in `docs/V2_PROGRESS.md`.
See `docs/V2_PROGRESS.md` for remaining phases: reconciliation rules, DLQ/backoff, richer observability, security hardening, CI benchmark gate, and recorded load results.
Loading