This project is a scalable, event-driven microservices architecture for a ticketing platform built with Java 17 / Spring Boot 3. It handles user authentication, dynamic routing, booking management, and event inventory updates using asynchronous communication.
- Dynamic API Gateway (Spring Cloud Gateway): Database-driven routing with role-based access control (RBAC).
- Event-Driven Consistency: Uses the Transactional Outbox Pattern to ensure data consistency between PostgreSQL and Kafka.
- Centralized Auth: Keycloak integration for Identity and Access Management (IAM).
- Docker Compose: Fully containerized setup with health checks and internal networking.
┌─────────────────────┐
│ Client/Browser │
└──────────┬──────────┘
│
│ HTTP + JWT
▼
┌────────────────────────────────────┐
│ KEYCLOAK (Port 8088) │
│ ┌────────────────────────────┐ │
│ │ • User Authentication │ │
│ │ • JWT Token Generation │ │
│ │ • Role Management │ │
│ └────────────────────────────┘ │
└────────────────┬───────────────────┘
│ JWT Token
▼
┌────────────────────────────────────┐
│ API GATEWAY (Port 8083) │
│ ┌────────────────────────────┐ │
│ │ • JWT Validation │ │
│ │ • Dynamic Routing │ │
│ │ • RBAC │ │
│ └────────────────────────────┘ │
└─┬──────────────┬──────────────┬───┘
│ │ │
┌───────────┘ │ └───────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ BOOKING SERVICE │ │ EVENT SERVICE │ │ USER SERVICE │
│ (Port 8081) │ │ (Port 8082) │ │ (Port 8084) │
│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ • Bookings │ │ │ │ • Venues │ │ │ │ • User Profile │ │
│ │ • Payments │ │ │ │ • Events │ │ │ │ • Registration │ │
│ │ • Outbox │ │ │ │ • Tickets │ │ │ │ │ │
│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
└──────────┬───────────┘ └──────────┬───────────┘ └──────────┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ booking_db │ │ event_db │ │ user_db │
│ (PostgreSQL) │ │ (PostgreSQL) │ │ (PostgreSQL) │
└──────────┬───────────┘ └──────────────────────┘ └──────────────────────┘
│
│ Outbox Scheduler (5s)
▼
┌─────────────────────────────────────────────────────────┐
│ KAFKA BROKER (Port 9092) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Topics: │ │
│ │ • booking.events │ │
│ │ • event.updates │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────┘
│ Consumes
▼
┌────────────────────────┐
│ EVENT SERVICE │
│ Kafka Consumer │
└────────────────────────┘
| Service | Technology | Port | Responsibility |
|---|---|---|---|
| API Gateway | Spring Cloud Gateway | 8083 |
Entry point. Handles routing lookup, JWT validation, and RBAC. |
| Booking Service | Spring Boot / Data JPA | 8081 |
Manages orders. Implements the Outbox Producer. |
| Event Service | Spring Boot / Data JPA | 8082 |
Manages Venues/Events. Implements the Kafka Consumer. |
| Keycloak | OIDC Provider | 8088 |
Handles user login, registration, and token generation. |
| Kafka | Message Broker | 9092 |
Decouples the Booking service from the Event service. |
╔════════════════════════════════════════════╗
║ api_routes ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • path_prefix VARCHAR(255) ║
║ • http_method VARCHAR(20) ║
║ • target_base_url VARCHAR(255) ║
║ • required_role VARCHAR(50) ║
║ • created_at TIMESTAMP ║
║ • updated_at TIMESTAMP ║
╚════════════════════════════════════════════╝
╔════════════════════════════════════════════╗
║ venues ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • name VARCHAR(255) ║
║ • address VARCHAR(255) ║
║ • city VARCHAR(100) ║
║ • capacity INT ║
║ • created_at TIMESTAMP ║
║ • updated_at TIMESTAMP ║
╚════════════════════════════════════════════╝
│
│ 1:N
▼
╔════════════════════════════════════════════╗
║ events ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • organizer_id VARCHAR(255) ║
║ • venue_id UUID (FK → venues) ║
║ • title VARCHAR(255) ║
║ • description TEXT ║
║ • category VARCHAR(64) ║
║ • start_time TIMESTAMP ║
║ • end_time TIMESTAMP ║
║ • status VARCHAR(32) ║
║ • created_at TIMESTAMP ║
║ • updated_at TIMESTAMP ║
╚════════════════════════════════════════════╝
│
│ 1:N
▼
╔════════════════════════════════════════════╗
║ ticket_types ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • event_id UUID (FK → events) ║
║ • name VARCHAR(100) ║
║ • price NUMERIC(10,2) ║
║ • currency VARCHAR(16) ║
║ • total_quantity INT ║
║ • remaining_quantity INT ║
║ • created_at TIMESTAMP ║
║ • updated_at TIMESTAMP ║
╚════════════════════════════════════════════╝
╔════════════════════════════════════════════╗
║ bookings ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • user_id UUID ║
║ • event_id UUID ║
║ • status VARCHAR(32) ║
║ • total_amount NUMERIC(10,2) ║
║ • currency VARCHAR(16) ║
║ • payment_status VARCHAR(32) ║
║ • payment_ref VARCHAR(255) ║
║ • created_at TIMESTAMP ║
║ • updated_at TIMESTAMP ║
╚════════════════════════════════════════════╝
│
│ 1:N
▼
╔════════════════════════════════════════════╗
║ booking_items ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • booking_id UUID (FK → bookings) ║
║ • ticket_type_id UUID ║
║ • quantity INT ║
║ • unit_price NUMERIC(10,2) ║
║ • total_price NUMERIC(10,2) ║
╚════════════════════════════════════════════╝
╔════════════════════════════════════════════╗
║ outbox_events ║
║ (Transactional Outbox) ║
╠════════════════════════════════════════════╣
║ • id UUID (PK) ║
║ • aggregate_type VARCHAR(64) ║
║ • aggregate_id UUID ║
║ • event_type VARCHAR(64) ║
║ • payload JSONB ║
║ • status VARCHAR(32) ║
║ • created_at TIMESTAMP ║
║ • processed_at TIMESTAMP ║
╚════════════════════════════════════════════╝
┌───────────────────────────────────────────────────────────────────┐
│ BOOKING SERVICE │
│ │
│ User Request → Create Booking │
│ │ │
│ ▼ │
│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
│ ┃ @Transactional saveBooking() ┃ │
│ ┃ ┌──────────────────────────────────────────────┐ ┃ │
│ ┃ │ 1. INSERT INTO bookings │ ┃ │
│ ┃ │ 2. INSERT INTO booking_items │ ┃ │
│ ┃ │ 3. INSERT INTO outbox_events │ ┃ │
│ ┃ │ - status: 'PENDING' │ ┃ │
│ ┃ │ - event_type: 'BOOKING_CREATED' │ ┃ │
│ ┃ │ - payload: {booking details} │ ┃ │
│ ┃ └──────────────────────────────────────────────┘ ┃ │
│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
│ │ │
│ │ COMMIT (All or Nothing) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ OutboxScheduler (@Scheduled fixedDelay = 5000ms) │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 1. SELECT * FROM outbox_events │ │ │
│ │ │ WHERE status = 'PENDING' │ │ │
│ │ │ │ │ │
│ │ │ 2. FOR EACH event: │ │ │
│ │ │ → Send to Kafka │ │ │
│ │ │ → UPDATE status = 'PUBLISHED' │ │ │
│ │ │ → SET processed_at = NOW() │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
└─────────┼─────────────────────────────────────────────────────────┘
│
│ Kafka Message
│ ┌───────────────────────────────────────┐
└►│ Topic: booking.events │
│ Key: booking-uuid │
│ Value: { │
│ "eventType": "BOOKING_CREATED", │
│ "bookingId": "...", │
│ "eventId": "...", │
│ "items": [...] │
│ } │
└───────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ KAFKA BROKER (Port 9092) │
│ ┌───────────────────────────────────────────────┐ │
│ │ 📋 booking.events │ │
│ │ - BOOKING_CREATED │ │
│ │ - BOOKING_CANCELLED │ │
│ │ - BOOKING_CONFIRMED │ │
│ └───────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│
│ Consumer Group: event-service-group
▼
┌───────────────────────────────────────────────────────────────────┐
│ EVENT SERVICE │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ @KafkaListener(topics = "booking.events") │ │
│ │ │ │
│ │ handleBookingEvent(BookingEvent event) { │ │
│ │ │ │
│ │ switch(event.getEventType()) { │ │
│ │ │ │
│ │ case BOOKING_CREATED: │ │
│ │ // Decrease ticket inventory │ │
│ │ UPDATE ticket_types │ │
│ │ SET remaining_quantity -= qty │ │
│ │ WHERE id = ticket_type_id │ │
│ │ │ │
│ │ case BOOKING_CANCELLED: │ │
│ │ // Restore ticket inventory │ │
│ │ UPDATE ticket_types │ │
│ │ SET remaining_quantity += qty │ │
│ │ WHERE id = ticket_type_id │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
✓ Guaranteed Delivery ✓ Eventual Consistency ✓ Decoupling
Key Benefits:
- Guaranteed Delivery: Outbox pattern ensures events are never lost
- Eventual Consistency: Services stay in sync asynchronously
- Decoupling: Services don't need to know about each other
- Scalability: Can add more consumers without changing producers
╔═══════════════════════════════════════════════════════════════════════╗
║ STEP 1: User Login & Token Generation ║
╚═══════════════════════════════════════════════════════════════════════╝
┌─────────────┐ ┌──────────────────┐
│ Client │ │ Keycloak │
│ (User) │ │ Port 8088 │
└──────┬──────┘ └────────┬─────────┘
│ │
│ POST /realms/ticketing-realm/protocol/... │
│ ┌──────────────────────────────────────┐ │
│ │ grant_type: password │ │
│ │ username: testuser │ │
│ │ password: password │ │
│ │ client_id: ticketing-client │ │
│ └──────────────────────────────────────┘ │
├────────────────────────────────────────────►│
│ │
│ │ ✓ Validate
│ │ Credentials
│ │
│ ◄ JWT Token Response │
│◄────────────────────────────────────────────┤
│ ┌──────────────────────────────────────┐ │
│ │ access_token: eyJhbGci... │ │
│ │ token_type: Bearer │ │
│ │ expires_in: 300 │ │
│ └──────────────────────────────────────┘ │
│ │
╔═══════════════════════════════════════════════════════════════════════╗
║ STEP 2: API Request with JWT Validation ║
╚═══════════════════════════════════════════════════════════════════════╝
┌─────────────┐ ┌────────────────┐ ┌──────────────────┐
│ Client │ │ API Gateway │ │ Keycloak │
└──────┬──────┘ │ Port 8083 │ │ Port 8088 │
│ └────────┬───────┘ └────────┬─────────┘
│ │ │
│ POST /api/v1/bookings │
│ Authorization: Bearer eyJhbGci... │
├──────────────────────►│ │
│ │ │
│ │ ① Validate JWT │
│ │ • Check signature │
│ │ • Check expiration │
│ │ • Extract roles │
│ ├───────────────────────►│
│ │ │
│ │ ◄ JWKS (Public Keys) │
│ │◄───────────────────────┤
│ │ │
│ │ ② Lookup Route │
│ │ FROM api_routes │
│ │ WHERE path = '/api/v1/bookings'
│ │ │
│ │ ③ Check RBAC │
│ │ required: USER │
│ │ user has: [USER] │
│ │ ✓ Authorized │
│ │ │
╔═══════════════════════════════════════════════════════════════════════╗
║ STEP 3: Forward to Backend Service ║
╚═══════════════════════════════════════════════════════════════════════╝
│ │ ┌──────────────────────┐
│ │ │ Booking Service │
│ │ │ Port 8081 │
│ │ └──────────┬───────────┘
│ │ │
│ │ ④ Forward Request │
│ │ POST http://booking-service:8081/...
│ │ + JWT in header │
│ ├────────────────────────►│
│ │ │
│ │ │ ⚙ Process
│ │ │ Business
│ │ │ Logic
│ │ │
│ │ ◄ 201 Created │
│ │◄────────────────────────┤
│ │ { "bookingId": "..." }│
│ │ │
│ ◄ Response │ │
│◄──────────────────────┤ │
│ { "bookingId": "..." } │
│ │ │
Security Features:
- JWT Validation: API Gateway verifies token signature using Keycloak's public keys
- Role-Based Access Control: Each route can specify required roles (USER, ADMIN, etc.)
- Token Expiration: Tokens expire after 5 minutes (configurable)
- Centralized Auth: All services trust Keycloak as the single source of truth
Because this system runs in Docker, we deal with "Split Horizon" DNS.
- Browser (You): Accesses Keycloak at
http://localhost:8088. The JWT Tokeniss(issuer) claim will behttp://localhost:8088.... - API Gateway (Docker): Needs to verify the token signature. It cannot talk to
localhost(that would be itself). It must talk to the container namedkeycloak.
This is handled in the API Gateway's docker-compose environment variables:
# 1. The token claims it comes from here (Validation check)
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://localhost:8088/realms/ticketing-realm
# 2. But we fetch the public keys from here (Physical network location)
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8088/realms/ticketing-realm/protocol/openid-connect/certs/project-root
├── docker-compose.yml
├── postgres-init.sql
├── /api-gateway
│ ├── src/main/java/com/example/gateway
│ │ ├── config # SecurityConfig.java (OAuth2)
│ │ ├── model # ApiRoute.java
│ │ ├── repository # RouteRepository.java
│ │ └── service # DynamicRouteLocator.java
│ └── Dockerfile
├── /booking-service
│ ├── src/main/java/com/example/booking
│ │ ├── controller
│ │ ├── service # BookingService (writes to DB + Outbox)
│ │ ├── kafka # OutboxScheduler.java (Polls DB -> Kafka)
│ │ └── entity # Booking.java, OutboxEvent.java
│ └── Dockerfile
├── /event-service
│ ├── src/main/java/com/example/event
│ │ ├── kafka # KafkaConsumer.java (@KafkaListener)
│ │ └── service # InventoryService.java
│ └── Dockerfile
1. Create a Route (SQL in gateway_db)
Connect to the Postgres container to run this:
INSERT INTO api_routes (id, path_prefix, http_method, target_base_url, required_role)
VALUES (gen_random_uuid(), '/api/v1/bookings', 'POST', 'http://booking-service:8081', 'USER');2. Get Token
TOKEN=$(curl -s -d "client_id=ticketing-client" -d "username=testuser" -d "password=password" -d "grant_type=password" \
http://localhost:8088/realms/ticketing-realm/protocol/openid-connect/token | jq -r .access_token)3. Call API
curl -X POST http://localhost:8083/api/v1/bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "123",
"items": [{"ticket_type_id": "abc", "quantity": 1}]
}'