High-concurrency ticket booking system built with Java 21 + Spring Boot + Domain-Driven Design (DDD).
A follow-along project based on the series by tipjs/anonystick.
Last completed: ep11 (Guava L1 cache + virtual threads) + ep12 (extreme benchmark tuning)
Next episode: ep13 — ELK Logs for distributed system
| What | Detail |
|---|---|
| Guava L1 cache | Added in TicketDetailCacheService — local in-memory cache before hitting Redis |
| 2-level cache flow | Guava (L1) → Redis (L2) → Redisson lock → MySQL |
| Virtual threads | spring.threads.virtual.enabled: true in application.yml |
| Extreme benchmark | New extreme mode: 2000 VUs × 2m with staged ramp-up in run-benchmark.ps1 / .sh |
| Tomcat tuning | server.tomcat.accept-count: 2000 to prevent connection refused under high load |
- k6 extreme mode (2000 VUs, no ramp-up) was causing "connection refused"
- Diagnosed via
netstat:ESTABLISHEDdropped from 4002 → 2 for ~15 seconds, then recovered - Root cause:
server.tomcat.accept-countdefaulted to 100 — OS rejected connections beyond the queue - Fix: increased to
2000+ added staged ramp-up in k6 script
# 1. Start Docker services
docker-compose -f environment/docker-compose-dev.yml up -d
# 2. Build
.\mvnw.cmd clean install -DskipTests
# 3. Run app
.\mvnw.cmd spring-boot:run -pl vetautet-start
# 4. Verify monitoring stack
# Prometheus : http://localhost:9090
# Grafana : http://localhost:3000 (admin / admin)
# App : http://localhost:1122/swagger-ui.html
# 5. Continue: ep13 — ELK Logs
# https://www.youtube.com/watch?v=6DGnzYkK0uQThis project simulates a real-world Tết train ticket booking system (vetautet.com) — one of the highest-concurrency scenarios in Vietnam's e-commerce space, where thousands of users compete for a limited number of tickets at the same time.
The goal is to practice building a scalable, resilient backend using clean architecture principles and modern Java ecosystem tooling.
The application follows Domain-Driven Design (DDD) and is split into 5 Maven modules:
spring-ddd-ticket-booking/
├── vetautet-start/ # Entry point, Spring Boot main, application.yml
├── vetautet-controller/ # REST controllers (@RestController)
├── vetautet-application/ # Application services, 2-level cache logic
├── vetautet-domain/ # Domain models, repository interfaces, domain services
├── vetautet-infrastructure/ # JPA, Redis, Redisson, DB implementations
├── environment/ # Docker Compose (MySQL, Redis, Prometheus, Grafana, exporters)
├── benchmark/ # k6 test results
├── knowledge-summary/ # Study notes, diagrams, screenshots per section
├── run-benchmark.ps1 # Load test runner (Windows)
└── run-benchmark.sh # Load test runner (Linux/macOS)
Request
│
▼
Guava (L1) ──hit──▶ return immediately (in-memory, ~0ms)
│ miss
▼
Redis (L2) ──hit──▶ populate L1 → return (~1ms)
│ miss
▼
Redisson distributed lock
│ locked
▼
MySQL ──▶ populate L2 + L1 → return
- L1 (Guava):
expireAfterAccess(10, MINUTES),concurrencyLevel= number of CPU cores - L2 (Redis):
TTL = 1 day, protected by Redisson distributed lock to prevent cache stampede
| Layer | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.5 |
| Architecture | Domain-Driven Design (DDD), Maven multi-module |
| Local Cache (L1) | Guava Cache |
| Distributed Cache (L2) | Redis (Redisson client) |
| Distributed Lock | Redisson |
| Database | MySQL 8.0 |
| Concurrency | Virtual Threads (spring.threads.virtual.enabled=true) |
| Resilience | Resilience4j (CircuitBreaker + RateLimiter) |
| Monitoring | Prometheus + Grafana |
| Exporters | mysqld-exporter, node-exporter, redis-exporter |
| API Docs | springdoc-openapi (Swagger UI) |
| Load Testing | k6 |
| CI/CD | GitHub Actions |
Start all services:
docker-compose -f environment/docker-compose-dev.yml up -d| Service | Container | Port | URL |
|---|---|---|---|
| MySQL 8.0 | pre-event-mysql |
3316 |
— |
| Redis | pre-event-redis |
6319 |
— |
| Prometheus | pre-event-prometheus |
9090 |
http://localhost:9090 |
| Grafana | pre-event-grafana |
3000 |
http://localhost:3000 (admin/admin) |
| node-exporter | pre-event-node-exporter |
9100 |
— |
| mysqld-exporter | pre-event-mysqld-exporter |
9104 |
— |
| redis-exporter | pre-event-redis-exporter |
9121 |
— |
| Spring Boot App | — | 1122 |
http://localhost:1122 |
| Dashboard | ID |
|---|---|
| JVM (Micrometer) | 4701 |
| MySQL Overview | via mysqld-exporter |
| Redis Overview | via redis-exporter |
- 2-Level Cache — Guava (L1 local) + Redis (L2 distributed) to reduce DB load and latency
- Virtual Threads — Java 21 virtual threads for higher concurrency with lower resource usage
- Distributed Lock — Redisson prevents cache stampede on cache miss under high concurrency
- Rate Limiter — Resilience4j controls request rate to protect the system on sale day
- Circuit Breaker — Resilience4j prevents cascading failures when downstream services degrade
- Monitoring Stack — Prometheus scrapes metrics; Grafana visualizes JVM, MySQL, Redis health
- DDD Structure — clean separation of domain logic from infrastructure concerns
- k6 installed
# Smoke test — 50 VUs x 5s
.\run-benchmark.ps1
# Standard benchmark — 50 VUs x 30s
.\run-benchmark.ps1 -Mode normal
# Stress test — 200 VUs x 30s
.\run-benchmark.ps1 -Mode heavy
# Extreme test — 2000 VUs x 2m (staged ramp-up)
.\run-benchmark.ps1 -Mode extreme
# Against prod
.\run-benchmark.ps1 -Target prod -Mode normalchmod +x run-benchmark.sh
./run-benchmark.sh # normal (default)
./run-benchmark.sh heavy # stress
./run-benchmark.sh extreme # 2000 VUs
./run-benchmark.sh normal prod # prod target0s ──── 30s: ramp up 0 → 2000 VUs
30s ─── 90s: sustain 2000 VUs
90s ── 120s: ramp down 2000 → 0 VUs
Staged ramp-up prevents OS-level "connection refused" caused by 2000 connections hitting the server simultaneously.
Tomcataccept-countis also tuned to2000(default was100).
| Metric | Good | Investigate |
|---|---|---|
http_req_failed |
0% | > 1% |
p(95) latency |
< 200ms | > 1s |
http_reqs/s |
stable / increasing | dropping |
| Mode | VUs | Duration | Throughput | p(95) | Error rate | Notes |
|---|---|---|---|---|---|---|
| normal (cold) | 50 | 5s | ~2,600/s | 30ms | 0% | — |
| normal (warm) | 50 | 5s | ~2,879/s | 22ms | 0% | cache warmed |
| heavy | 200 | 30s | ~2,694/s | 123ms | 0.72% | bottleneck visible |
| extreme | 2000 | 2m | ~2,823/s | 2.46s | 1.17% | before ramp-up fix |
- Java 21+
- Docker & Docker Compose
mvnw.cmdwrapper included (no global Maven needed)
# Clone
git clone https://github.com/FongFox/spring-ddd-ticket-booking.git
cd spring-ddd-ticket-booking
# Start dependencies
docker-compose -f environment/docker-compose-dev.yml up -d
# Build all modules (install, not package — needed for cross-module JARs)
.\mvnw.cmd clean install -DskipTests
# Run
.\mvnw.cmd spring-boot:run -pl vetautet-startApp : http://localhost:1122/swagger-ui.html
Prometheus : http://localhost:9090
Grafana : http://localhost:3000
This project is a hands-on implementation following the Java DDD - Vé Tàu Tết series by tipjs:
- 🌐 Blog: anonystick.com
- 📺 YouTube: JAVA DDD Series
All credit for the architecture design and teaching material goes to tipjs/anonystick.
This repo is purely a learning exercise.
- Note: ✅ Done · ⏳ Todo
| Section | Topic | Link | Status |
|---|---|---|---|
| 01 | JAVA DDD 01: Xây dựng dự án DDD bán vé tàu, kiến trúc đồng thời cao | ▶ | ✅ Done |
| 02 | JAVA DDD 02: DDD Structure Project | ▶ | ✅ Done |
| 03 | Project bán vé tàu: API sập ngày đầu bán vé, review code | ▶ | ✅ Done |
| 04 | JAVA DDD 3: Hoàn thành setup Microservice | ▶ | ✅ Done |
| 05 | JAVA DDD 04: Circuit Breaker vs RateLimiter | ▶ | ✅ Done |
| 06 | Source Code ~1,000 QPS: Section 0–4 How to run | ▶ | ✅ Done |
| 07 | JAVA DDD 05: Distributed Cache — LUA vs Redisson | ▶ | ✅ Done |
| 08 | Distributed Cache Redis phản bội — 1 tỷ thất thoát | ▶ | ✅ Done |
| 10 | JAVA DDD 06: Vì sao không dùng LUA Redis | ▶ | ✅ Done |
| 11 | JAVA DDD 07: Setup Prometheus monitoring | ▶ | ✅ Done |
| 12 | JAVA DDD 08: Grafana — System Monitoring | ▶ | ✅ Done |
| 13 | JAVA DDD 09: Giám sát MySQL online | ▶ | ✅ Done |
| 14 | JAVA DDD 10: Giám sát Redis distributed | ▶ | ✅ Done |
| 15 | Source Code ~5,000 QPS: Section 4–10 How to run | ▶ | ✅ Done |
| 16 | JAVA DDD 11: Vũ khí tăng tốc 20,000 req/s — 5 tiêu chí | ▶ | ✅ Done |
| 17 | JAVA DDD 12: 25,000 req/s — Guava L1 + virtual threads | ▶ | ✅ Done |
| 18 | JAVA DDD 13: ELK Logs for distributed system | ▶ | ⏳ Todo ← resume here |
| 19 | JAVA DDD 14: Consistency — tính nhất quán thực tế | ▶ | ⏳ Todo |
| 20 | JAVA DDD 15: Nginx proxy + 2 server, StockAvailable không nhất quán | ▶ | ⏳ Todo |
| 21 | JAVA DDD 16: DEV SA, dữ liệu phân tán nhất quán — cách đơn giản | ▶ | ⏳ Todo |
| 24 | JAVA DDD 17: Triển khai mức nhất quán phù hợp (2) | ▶ | ⏳ Todo |
| 25 | Source Code ~15,000 QPS: Section 5–17 How to run | ▶ | ⏳ Todo |
MIT
Feel free to use this code for learning purposes. Please credit the original series if you share or build on top of it.