SeatLock is a hexagonal backend service that provides a safe seat holding API for events.
It focuses on:
- Strict business rules (max 5 seats per reservation, expiration windows, etc.)
- No overselling via pessimistic locking and transaction retries
- Idempotent HTTP APIs with Redis-backed storage
- Clear observability and production-grade infrastructure
This repository is the single source of truth for both the code and the documentation.
Get a feel for the project without reading all the docs.
Spin everything up:
make init
make up-warmThen explore:
Interactive OpenAPI / Swagger UI powered by Nelmio.
Demonstrates listing seats, holding reservations, idempotency, and error handling.
Dockerized stack with PostgreSQL, Redis, RabbitMQ, MinIO and full observability (Grafana, Prometheus, Loki, Tempo).
Shows containers running and how metrics, logs and traces are wired together.
k6 script simulating concurrent users hitting the hold seats endpoint.
Result: zero overselling, only 201 Created and 409 Conflict responses.
Verifies race condition prevention via row-level locks and retry logic.
- 200+ tests (Pest)
- ~90%+ coverage
- PHPStan level 8
- Pint code style
All enforced locally and in CI.
Runs the full test suite, coverage, static analysis and linting.
Domain events are written to an outbox table and delivered asynchronously to RabbitMQ using Symfony Messenger.
Flow: HTTP request → DB transaction → outbox insert → worker relay → RabbitMQ.
- Docker and Docker Compose
- Make
- PHP and Composer (only if you want to run the app without Docker)
git clone https://github.com/alisolphp/SeatLock seatlock
cd seatlock
make installStart the application and its dependencies (PostgreSQL, Redis, RabbitMQ, MinIO, etc.):
make upStop everything:
make downOnce the stack is up:
- Base URL (dev):
http://localhost:8080/
Example API endpoints (see API.md for full details):
GET /api/v1/events/{eventId}/seats– list available seats for an eventPOST /api/v1/events/{eventId}/hold– hold up to 5 seats with idempotency
OpenAPI / Swagger (via Nelmio):
- JSON:
GET /api/doc.json - UI (if enabled):
GET /api/doc
Use these together with API.md when integrating clients or doing manual testing.
These commands are enforced locally and in CI. Run them before opening a PR.
Run the full test suite:
make testOptional human-friendly output:
make test-doxRun tests with coverage (must stay at or above 90%):
make coverageCoverage is validated in CI; if it drops below the threshold, the pipeline fails.
Run PHPStan at level 8:
make analyseRun Pint:
make lintCI will fail if style checks do not pass.
You can understand the whole system by reading a small set of focused docs.
| File | What you’ll find | When to read it |
|---|---|---|
ARCHITECTURE.md |
Why Symfony + Hexagonal, locking strategy, retry logic, idempotency, outbox, Redis keys | Read first – full mental model |
DOMAIN.md |
Entities, Value Objects, Enums, invariants, Domain Events payloads | When working on business logic or events |
API.md |
Full API spec, endpoints, request/response examples, error codes, idempotency rules | When building or consuming the API |
INFRASTRUCTURE.md |
Observability stack, logging, health checks, OpenTelemetry conventions | When debugging infra or adding tracing/metrics |
DIRECTORY_STRUCTURE.md |
Exact folder layout and what belongs where (strict Hexagonal) | When adding or moving classes/files |
| File | What you’ll find |
|---|---|
CONTRIBUTING.md |
Conventional Commits, branching strategy, code review expectations |
PHASE0_TASKS.md |
Phase 0 checklist and what “done” means for the initial milestone |
TESTING.md |
Testing strategy, tools, and conventions |
SECURITY.md |
Security model, headers, CORS, hardening guidelines |
BACKUP_RECOVERY.md |
Backup and disaster recovery plan |
docs/adr/
├── 001-framework-choice.md # Symfony instead of Laravel
├── 002-locking-strategy.md # Pessimistic locking + retry
└── 003-outbox-pattern.md # Messenger + doctrine transport
Only “real” decisions live here; everything else is folded into the main docs.
- HTTP client calls a controller.
- Controller delegates to an Application
UseCase. - The
UseCaseruns inside aTransactionManagerthat handles retries on transient errors. - The
UseCasetalks to Domain Services and Repository interfaces (pure domain layer). - The Infrastructure layer (Doctrine, Redis, Messenger, etc.) provides concrete adapters.
- Domain Events are persisted to an outbox table and relayed by a worker to RabbitMQ.
That’s the core flow. Everything else is detail.
Configuration is mostly driven by .env variables and Symfony config under config/:
- Database, Redis, RabbitMQ, MinIO and other services are wired via Docker Compose.
- CORS is configured via NelmioCorsBundle and environment-specific settings.
- Logging and tracing follow the conventions documented in
INFRASTRUCTURE.md.
For environment-specific behavior (dev, test, prod), see the config/packages and docker directories.
For a deeper understanding, these diagrams live under docs/assets:
-
We follow Conventional Commits strictly. See
docs/CONTRIBUTING.mdfor allowed types and examples. -
Before pushing:
- Ensure tests pass:
make test,make coverage. - Ensure static analysis and style checks pass:
make analyse,make lint.
- Ensure tests pass:
-
Keep documentation changes small and focused. The goal is to keep everything:
- Concise
- Non-redundant
- Developer-friendly
This project is licensed under the MIT License – see the LICENSE file for details.








