- Prerequisites
- Installation
- Infrastructure Services
- Running Individual Services Locally
- Seeding Data
- End-to-End Booking Walkthrough
- Viewing Emails in Mailpit
- Viewing Kafka Messages
- Hot Reload
- Running Tests
- Troubleshooting
| Tool | Minimum Version | Check | Install |
|---|---|---|---|
| Docker | 24.x | docker --version |
docs.docker.com |
| Docker Compose | 2.x | docker compose version |
Bundled with Docker Desktop |
| Make | 3.x | make --version |
brew install make |
| Bun | 1.1+ | bun --version |
curl -fsSL https://bun.sh/install | bash |
| JDK | 21 | java --version |
sdk install java 21-graalce (SDKMAN) |
| Python | 3.12 | python3 --version |
pyenv install 3.12 |
| Maven | 3.9+ | mvn --version |
brew install maven |
For Docker Compose development you only need Docker + Make. Language runtimes are required only for running services outside containers.
git clone https://github.com/your-org/ticketflow.git
cd ticketflowmake setup
# Equivalent to: cp .env.example .envReview .env and update any values you want to customise. Defaults work for local development without changes.
make devThis runs docker compose up --build and waits for all health checks to pass. First run downloads ~2 GB of images and may take 3–5 minutes. Subsequent runs start in under 30 seconds.
make healthmake seedCreates:
- 2 venues
- 4 events with seat maps
- 1 test user (
test@ticketflow.dev/Test1234!)
When make dev runs, the following infrastructure containers start alongside the application services:
| Container | Purpose | Port |
|---|---|---|
kafka |
Message broker | 9092 (internal), 29092 (host) |
zookeeper |
Kafka coordination | 2181 |
postgres |
Relational database | 5432 |
mongodb |
Document database | 27017 |
redis |
Seat lock cache | 6379 |
redpanda-console |
Kafka UI | 8080 |
mongo-express |
MongoDB UI | 8081 |
mailpit |
Email capture | 8025 (UI), 1025 (SMTP) |
prometheus |
Metrics scraper | 9090 |
grafana |
Dashboards | 3010 |
jaeger |
Distributed tracing | 16686 |
loki |
Log aggregation | 3100 |
promtail |
Log shipper | — |
# Stop everything
make down
# Start only infrastructure (no application services)
make infra
# Start application services (assumes infra is running)
make servicesRun a service outside Docker when you want faster iteration or need to attach a debugger. The service connects to the Dockerised infrastructure.
Ensure infrastructure is running:
make infraExport shared environment variables (or source .env):
export $(grep -v '^#' .env | xargs)cd gateway
bun install
bun dev
# Hot reload via Bun's built-in watcher
# Service available at http://localhost:3000cd services/user-service
mvn spring-boot:run
# Or with a specific profile:
mvn spring-boot:run -Dspring-boot.run.profiles=local
# Service available at http://localhost:3001To enable the Spring Boot DevTools live-reload:
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"Then attach your IDE debugger to port 5005.
cd services/event-service
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 3002
# Hot reload via uvicorn --reload
# Service available at http://localhost:3002
# OpenAPI docs: http://localhost:3002/docscd services/booking-service
mvn spring-boot:run
# Service available at http://localhost:3003cd services/inventory-service
bun install
bun dev
# Service available at http://localhost:3004cd services/payment-service
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 3005
# Service available at http://localhost:3005
# OpenAPI docs: http://localhost:3005/docscd services/notification-service
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 3006
# Service available at http://localhost:3006make seed
# Equivalent to: bash scripts/seed.shThe seed script:
- Registers the test user.
- Creates two venues (Madison Square Garden, O2 Arena).
- Creates four events with full seat maps.
To reset and re-seed:
make reset-db
make seedWarning:
make reset-dbdrops all databases. Do not run in production.
The following curl sequence walks through the entire booking saga manually.
curl -s -X POST http://localhost:3000/api/users/register \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Example",
"email": "alice@example.com",
"password": "Alice1234!"
}' | jq .TOKEN=$(curl -s -X POST http://localhost:3000/api/users/login \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "password": "Alice1234!"}' \
| jq -r '.token')
echo "Token: $TOKEN"curl -s http://localhost:3000/api/events | jq '.[] | {id, name, date}'Note an event ID from the output, e.g. EVENT_ID=evt_01HX....
curl -s http://localhost:3000/api/inventory/events/$EVENT_ID/seats \
| jq '[.[] | select(.status == "AVAILABLE")] | .[0:5]'Note 2–3 seat IDs.
BOOKING_RESPONSE=$(curl -s -X POST http://localhost:3000/api/bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"eventId\": \"$EVENT_ID\",
\"seatIds\": [\"seat_01\", \"seat_02\"],
\"paymentMethod\": {
\"type\": \"card\",
\"token\": \"tok_visa\"
}
}")
echo $BOOKING_RESPONSE | jq .
BOOKING_ID=$(echo $BOOKING_RESPONSE | jq -r '.bookingId')The response is 202 Accepted with a bookingId. The saga is now running asynchronously.
# Poll every 2 seconds until status is CONFIRMED or FAILED
for i in {1..15}; do
STATUS=$(curl -s http://localhost:3000/api/bookings/$BOOKING_ID \
-H "Authorization: Bearer $TOKEN" | jq -r '.status')
echo "Attempt $i: $STATUS"
[ "$STATUS" = "CONFIRMED" ] || [ "$STATUS" = "FAILED" ] && break
sleep 2
doneOpen http://localhost:8025 in your browser. You should see a booking confirmation email.
Mailpit is a local SMTP server and web UI that captures all outbound email sent by the Notification Service.
- URL: http://localhost:8025
- All email sent during local development is captured here.
- No real email is sent; Mailpit acts as a sink.
- Use the web UI to inspect HTML/text bodies, headers, and attachments.
To reset the Mailpit inbox:
curl -s -X DELETE http://localhost:8025/api/v1/messagesOpen http://localhost:8080 in your browser.
- Topics tab — browse all topics, view messages, offsets, partition distribution.
- Consumer Groups tab — view consumer lag per group per partition. High lag indicates a slow or stopped consumer.
- Schema Registry tab — not used (schemas are documented in
docs/event-catalog.md).
# List topics
docker exec -it ticketflow-kafka-1 kafka-topics.sh \
--bootstrap-server localhost:9092 --list
# Consume the booking topic from the beginning
docker exec -it ticketflow-kafka-1 kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic ticketflow.booking.initiated \
--from-beginning \
--property print.key=true| Service | Hot Reload | Mechanism |
|---|---|---|
| API Gateway | Yes | Bun built-in --watch |
| User Service | Partial | Spring Boot DevTools (class reload, no full restart) |
| Event Service | Yes | uvicorn --reload watches app/ directory |
| Booking Service | Partial | Spring Boot DevTools |
| Inventory Service | Yes | Bun built-in --watch |
| Payment Service | Yes | uvicorn --reload |
| Notification Service | Yes | uvicorn --reload |
| Frontend | Yes | Vite HMR |
When running in Docker Compose (via make dev), the source directories are bind-mounted into containers where supported, so hot reload works without rebuilding images.
# All tests (runs in Docker to match CI)
make test
# Unit tests for a specific service
cd services/booking-service && mvn test
cd services/event-service && pytest
cd gateway && bun test
# Integration tests (requires running infra)
make test-integration- Check consumer group lag in Redpanda Console at http://localhost:8080.
- Check the service logs:
docker compose logs -f booking-service. - Ensure the topic exists: browse Topics in Redpanda Console.
- Check if the consumer crashed due to a deserialization error — look for
DeserializationExceptionin logs.
- Check Redis is healthy:
docker compose ps redis. - Check Redis connectivity from inventory service:
docker exec -it ticketflow-inventory-service-1 sh -c 'redis-cli -h redis ping'
- Check for stale locks from a previous crashed saga:
Delete stale keys:
docker exec -it ticketflow-redis-1 redis-cli KEYS "seat:*"
docker exec -it ticketflow-redis-1 redis-cli DEL seat:evt_01:A1
- Open Redpanda Console and check the
ticketflow.booking.initiatedtopic for the booking's message. - Check Inventory Service logs for errors processing that message.
- Check the DLT topic
ticketflow.booking.initiated.DLTfor failed messages.
- Confirm the Booking Service produced a
ticketflow.payment.requestedmessage in Redpanda Console. - Check the
payment-serviceconsumer group lag. - Verify
STRIPE_SECRET_KEYis set in.env.
# Re-run PostgreSQL migrations
docker exec -it ticketflow-user-service-1 sh -c 'java -jar app.jar --spring.flyway.repair=true'
# Re-run MongoDB index creation
docker exec -it ticketflow-mongodb-1 mongosh --eval "load('/docker-entrypoint-initdb.d/init.js')"If port 3000 or another port is already in use:
# Find what is using port 3000
lsof -i :3000
# Change the port in .env and docker-compose.yml# Clear build cache and rebuild
docker compose down -v
docker builder prune -f
make dev- JWT is decoded, not verified at the gateway. The gateway uses
josedecodeJwt()— no signature verification. It injectsx-user-id,x-user-email,x-user-roleheaders. Downstream services trust these headers unconditionally.JWT_SECRETis only consumed byuser-serviceandbooking-service. - Payment is mocked.
PAYMENT_SUCCESS_RATE=0.95controls success probability. Set to1.0for deterministic success. Razorpay integration is configured but falls back to mock when no API keys are provided. - Kafka external port is 9093, not 9092.
localhost:9093from the host;kafka:9092inside Docker. - Redis has no password in dev. The dev compose file omits
--requirepass. Prod usesredis://:${REDIS_PASSWORD}@redis:6379. - Inventory DB migration is explicit. First local run outside Docker:
bun src/db/migrate.tsfromservices/inventory-service/. JWT_EXPIRATIONis in milliseconds.604800000= 7 days. The.env.examplementionsJWT_EXPIRY_HOURSbut onlyJWT_EXPIRATION(ms) is consumed.make dev-cleandeletes all volumes. Data is lost. Requires re-runningmake seed.- No CI pipeline. No
.github/directory. No pre-commit hooks. - Production compose is
docker-compose.yml. Notdocker-compose.prod.yml.