A ticket sales platform built with a microservices architecture in Go, featuring event-driven communication via CDC (Change Data Capture), distributed locking for ticket reservations, and full-text search powered by Elasticsearch.
- Architecture Overview
- Tech Stack
- Entity Relationship Diagram
- Services
- Flowcharts
- Running the Project
- API Reference
The system is composed of 3 independent Go microservices that share a single PostgreSQL database and communicate asynchronously via Kafka (CDC):
βββββββββββ ββββββββββββββββββββββββ βββββββββββββββ
β Client ββββββΆβ Event Management ββββββΆβ PostgreSQL β
β β β API (:8080) βββββββ β
βββββββββββ ββββββββββββ¬ββββββββββββ ββββββββ¬βββββββ
β β β
β ββββββββΌβββββββ ββββββββΌβββββββ
β β Redis β β Debezium β
β β (Cache + β β Connect β
β β Locks) β ββββββββ¬βββββββ
β ββββββββ²βββββββ β
β β ββββββββΌβββββββ
β ββββββββββββ΄ββββββββββββ β Kafka β
βββββββββββΆβ Booking API (:8081) β β β
β ββββββββββββββββββββββββ ββββββββ¬βββββββ
β β
β ββββββββββββββββββββββββ ββββββββΌββββββββββββ
βββββββββββΆβ Search API (:8082) ββββββΆβ Elasticsearch β
ββββββββββββββββββββββββ ββββββββββββββββββββ
Key architectural patterns:
| Pattern | Description |
|---|---|
| Cache-Aside | Redis caches event data (1h TTL) and search results (5min TTL) to reduce DB/ES load |
| Distributed Lock | Redis-based locks with 15-minute TTL for temporary ticket reservations |
| Two-Phase Booking | Reserve (temporary lock) β Confirm (persist to DB) |
| CDC (Change Data Capture) | Debezium captures PostgreSQL WAL changes β publishes to Kafka β Search API consumes and indexes into Elasticsearch |
| Clean Architecture | Each service follows: entities β repositories β services β usecases β handlers |
| Component | Technology | Version |
|---|---|---|
| Language | Go | 1.25 |
| HTTP Framework | Echo | v5 |
| ORM | GORM (with generics) | v1.31 |
| Database | PostgreSQL | 17 |
| Cache / Locks | Redis | 8 |
| Message Broker | Apache Kafka | 4.0 |
| CDC Connector | Debezium | 3.5 |
| Search Engine | Elasticsearch | 9.3 |
| Migrations | Goose | β |
| Hot Reload | Air | β |
| Containers | Docker Compose | β |
βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β venues β β performers β
βββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββββββ€
β PK id BIGINT β β PK id BIGINT β
β uuid TEXT UNIQUEβ β uuid TEXT UNIQUE β
β created_at TIMESTAMPTZβ β created_at TIMESTAMPTZ β
β updated_at TIMESTAMPTZβ β updated_at TIMESTAMPTZ β
β location TEXT β β name TEXT β
β seat_map JSON β β age BIGINT β
βββββββββββββββ¬ββββββββββββββ β description TEXT NULL β
β 1 ββββββββββββββββ¬βββββββββββββββββ
β β
β β M
β βββββββββββββββββ΄βββββββββββββββββ
β β event_performers β
β ββββββββββββββββββββββββββββββββββ€
β β PK,FK event_id BIGINT β
β β PK,FK performer_id BIGINT β
β βββββββββββββββββ¬βββββββββββββββββ
β β M
β β
β N ββββββββββββββββββββββββββββββββ€
ββββββββ€ events β
ββββββββββββββββββββββββββββββββ€
β PK id BIGINT β
β uuid TEXT UNIQUE β
β created_at TIMESTAMPTZ β
β updated_at TIMESTAMPTZ β
β FK venue_id BIGINT β
β date TIMESTAMPTZ β
β name TEXT β
β description TEXT NULL β
ββββββββββββββββ¬βββββββββββββββββ
β 1
β
β N
ββββββββββββββββ΄βββββββββββββββββ
β tickets β
βββββββββββββββββββββββββββββββββ€
β PK id BIGINT β
β uuid TEXT UNIQUE β
β created_at TIMESTAMPTZ β
β updated_at TIMESTAMPTZ β
β FK event_id BIGINT β
β price BIGINT β
β seat TEXT β
β status TEXT β
β (available|booked) β
ββββββββββββββββββββββββββββββββββ
| Relationship | Type | Cascade |
|---|---|---|
| venues β events | One-to-Many | ON UPDATE CASCADE, ON DELETE CASCADE |
| events β tickets | One-to-Many | ON UPDATE CASCADE, ON DELETE CASCADE |
| events β performers | Many-to-Many (via event_performers) |
ON UPDATE CASCADE, ON DELETE CASCADE |
venues: locationperformers: nameevents: venue_id, date, nametickets: event_id, seat, statusevent_performers: performer_id
Port: 8080 Β |Β Base path: /api/v1/management
Responsible for querying event details with all related data (venue, performers, tickets). Uses Redis cache (1h TTL) for event data and checks ticket reservation status in real-time via Redis locks.
Endpoint:
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/management/events/:uuid |
Find event by UUID with full details |
Response example:
{
"uuid": "d0e1f2a3-b4c5-4d5e-8f9a-0b1c2d3e4f5a",
"name": "The Eras Tour - New York",
"description": "Taylor Swift brings her iconic Eras Tour...",
"date": "2026-07-15T20:00:00Z",
"venue": {
"uuid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"location": "Madison Square Garden, New York",
"seat_map": { "A1": true, "A2": true, "B1": true }
},
"performers": [
{
"uuid": "e5f6a7b8-c9d0-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Taylor Swift",
"age": 34,
"description": "Multi-award winning pop and country music artist..."
}
],
"tickets": [
{ "uuid": "c5d6e7f8-...", "price": 35000, "seat": "A1", "status": "available" },
{ "uuid": "d6e7f8a9-...", "price": 35000, "seat": "A2", "status": "reserved" },
{ "uuid": "f8a9b0c1-...", "price": 30000, "seat": "B1", "status": "booked" }
]
}Note: The
statusfield on tickets reflects real-time state. A ticket can beavailable,reserved(temporarily locked in Redis), orbooked(confirmed in DB).
Port: 8081 Β |Β Base path: /api/v1/booking
Handles the two-phase ticket purchasing flow. Uses Redis distributed locks with a 15-minute TTL for temporary reservations.
Endpoints:
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/booking/tickets/reserve |
Reserve a ticket (temporary lock) |
POST |
/api/v1/booking/tickets/confirm |
Confirm purchase (persist to DB) |
Reserve request:
{
"ticket_uuid": "c5d6e7f8-a9b0-4c5d-8e9f-0a1b2c3d4e5f",
"user_uuid": "550e8400-e29b-41d4-a716-446655440000"
}Confirm request:
{
"ticket_uuid": "c5d6e7f8-a9b0-4c5d-8e9f-0a1b2c3d4e5f",
"user_uuid": "550e8400-e29b-41d4-a716-446655440000",
"payment_method_uuid": "660e8400-e29b-41d4-a716-446655440000"
}Response codes:
| Status | Meaning |
|---|---|
200 |
Success (no body) |
400 |
Invalid request payload |
409 |
Ticket not available (reserve) or not reserved for user (confirm) |
500 |
Internal server error |
Redis key structure:
| Key Pattern | Purpose | TTL |
|---|---|---|
ticket_lock:{ticket_uuid} |
Global lock β prevents other users from reserving | 15 min |
ticket_lock_user:{ticket_uuid}:{user_uuid} |
User-specific lock β validates ownership on confirm | 15 min |
Port: 8082 Β |Β Base path: /api/v1/search
Provides full-text search over events using Elasticsearch. Data is synced from PostgreSQL via CDC (Debezium β Kafka). Results are cached in Redis for 5 minutes.
Endpoint:
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/search/events?q=&page=&size= |
Search events by text query |
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
q |
string | β | Search query (matches name, description, location) |
page |
int | 1 | Page number |
size |
int | 10 | Results per page (max 100) |
Response example:
{
"events": [
{
"uuid": "d0e1f2a3-b4c5-4d5e-8f9a-0b1c2d3e4f5a",
"name": "The Eras Tour - New York",
"description": "Taylor Swift brings her iconic Eras Tour...",
"date": "2026-07-15T20:00:00Z",
"location": "Madison Square Garden, New York"
}
],
"total": 1,
"page": 1,
"size": 10
}Search behavior:
- Fuzzy matching (
fuzziness: AUTO) acrossname(4x boost),location(2x boost), anddescription - Only returns future events (
date >= now) - Sorted by relevance score (desc), then date (asc)
CDC Consumer β Kafka topics:
| Topic | Trigger | Action |
|---|---|---|
pg.public.events |
Event created/updated | Upsert document in Elasticsearch events index |
pg.public.events |
Event deleted | Delete document from Elasticsearch index |
pg.public.venues |
Venue updated | Update location field on all related event documents |
GET /events/:uuid
β
βΌ
βββββββββββββββββββ
β Check Redis β
β cache for UUID β
ββββββββββ¬βββββββββ
β
βββββββ΄ββββββ
β Cache hit? β
βββββββ¬ββββββ
Yes β No
ββββββββββββ΄βββββββββββ
βΌ βΌ
ββββββββββββββ ββββββββββββββββββββ
β Use cached β β Query PostgreSQL β
β event β β (with preloads: β
βββββββ¬βββββββ β venue, performers,β
β β tickets) β
β ββββββββββ¬βββββββββββ
β β
β βΌ
β ββββββββββββββββββ
β β Store in Redis β
β β cache (1h) β
β ββββββββββ¬ββββββββ
β β
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β Check Redis for each β
β ticket lock key β
β (ticket_lock:{uuid}) β
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β Mark locked tickets β
β as "reserved" β
ββββββββββββ¬ββββββββββββ
βΌ
Return EventDTO
POST /tickets/reserve
{ ticket_uuid, user_uuid }
β
βΌ
βββββββββββββββββββββ
β Check ticket β
β status in DB β
β (status=available?)β
βββββββββββ¬ββββββββββ
β
βββββββ΄ββββββ
β Available? β
βββββββ¬ββββββ
No β β Yes
βΌ βΌ
βββββββββββ ββββββββββββββββββββββββ
β 409 β β Redis Pipeline: β
β Conflictβ β SETNX ticket_lock: β
βββββββββββ β {uuid} (TTL 15min) β
β SETNX ticket_lock_ β
β user:{uuid}:{user} β
β (TTL 15min) β
ββββββββββββ¬ββββββββββββββ
β
βββββββ΄ββββββ
β Lock set? β
βββββββ¬ββββββ
No β β Yes
βΌ βΌ
βββββββββββ βββββββββββ
β 409 β β 200 OK β
β Conflictβ β β
βββββββββββ βββββββββββ
POST /tickets/confirm
{ ticket_uuid, user_uuid,
payment_method_uuid }
β
βΌ
ββββββββββββββββββββββββ
β Check Redis for β
β ticket_lock_user: β
β {ticket}:{user} β
ββββββββββββ¬ββββββββββββ
β
βββββββ΄ββββββ
β Reserved β
β by user? β
βββββββ¬ββββββ
No β β Yes
βΌ βΌ
βββββββββββ ββββββββββββββββββββ
β 409 β β TODO: Process β
β Conflictβ β payment via β
βββββββββββ β Stripe β
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ
β Update ticket β
β status β "booked"β
β in PostgreSQL β
ββββββββββ¬ββββββββββ
βΌ
βββββββββββ
β 200 OK β
βββββββββββ
GET /search/events?q=taylor&page=1&size=10
β
βΌ
βββββββββββββββββββββ
β Build cache key: β
β search:taylor:1:10β
βββββββββββ¬ββββββββββ
βΌ
βββββββββββββββββββββ
β Check Redis cache β
βββββββββββ¬ββββββββββ
β
βββββββ΄ββββββ
β Cache hit? β
βββββββ¬ββββββ
Yes β No
βββββββββββ΄ββββββββββ
βΌ βΌ
ββββββββββββ ββββββββββββββββββββββ
β Return β β Query Elasticsearchβ
β cached β β - multi_match on β
ββββββββββββ β name^4, location β
β ^2, description β
β - filter: dateβ₯now β
β - fuzziness: AUTO β
ββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββ
β Cache result in β
β Redis (5min TTL) β
ββββββββββ¬ββββββββββββ
βΌ
Return results
ββββββββββββββββ WAL ββββββββββββββββ Kafka ββββββββββββββ
β PostgreSQL βββββββββββββΆβ Debezium ββββββββββββΆβ Kafka β
β β (logical β Connect β topics: β β
β tables: β replicat.) β β pg.public. β pg.public. β
β - events β β Monitors: β events β events β
β - venues β β - events β pg.public. β pg.public. β
β β β - venues β venues β venues β
ββββββββββββββββ ββββββββββββββββ ββββββββ¬ββββββ
β
βΌ
ββββββββββββββββββ
β Search API β
β CDC Consumer β
ββββββββββ¬ββββββββ
β
βββββββββββββββββββββββββββββ¬βββββββ΄βββββββββ
βΌ βΌ βΌ
ββββββββββββββββββββ βββββββββββββββββ ββββββββββββββββ
β op=c or op=u β β op=d β β venue op=u β
β (create/update) β β (delete) β β β
β β β β β β
β Upsert event doc β β Delete event β β Update all β
β in Elasticsearch β β from ES index β β events with β
β (fetch venue for β β β β venue_id β β
β location) β β β β new location β
ββββββββββββββββββββ βββββββββββββββββ ββββββββββββββββ
- Docker and Docker Compose
- Go 1.25+ (for local development)
- Goose (database migrations)
- Air (hot reload, optional for local dev)
This is the easiest way to run the entire platform:
# 1. Start all containers (infra + APIs)
make up
# 2. Wait for services, then run migrations + create Debezium connector + seed data
make setupServices will be available at:
| Service | URL |
|---|---|
| Event Management API | http://localhost:8080 |
| Booking API | http://localhost:8081 |
| Search API | http://localhost:8082 |
| Elasticsearch | http://localhost:9200 |
| Kibana | http://localhost:5601 |
| Kafka Connect | http://localhost:8083 |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
Useful commands:
# Check migration status
make migrate-status
# Check Debezium connector status
make connector-status
# Stop all containers
make down
# Stop and remove all data (volumes)
make reset
# Full teardown (delete connector + rollback migrations)
make teardownFor developing a single service with hot reload:
# 1. Start only infrastructure containers
make up-components
# 2. Run migrations and setup CDC
make setup
# 3. Navigate to the desired service and start with hot reload
cd event-management && make run-dev # starts on :8080
cd booking && make run-dev # starts on :8080 (use different port or docker)
cd search && make run-dev # starts on :8080Note: When running services locally, you need to set environment variables or create a
.envfile matching the variables fromdocker-compose.yml(e.g.,POSTGRES_HOST=localhost).
Environment variables required by each service:
Event Management & Booking
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=main
POSTGRES_PASSWORD=password
POSTGRES_DBNAME=event-management
POSTGRES_SSLMODE=disable
POSTGRES_TIMEZONE=UTC
POSTGRES_MAX_OPEN_CONNS=25
POSTGRES_CONN_MAX_LIFETIME=300
POSTGRES_MAX_IDLE_CONNS=10
POSTGRES_CONN_MAX_IDLE_TIME=60
SERVER_MODE=development
SERVER_ADDR=:8080
SERVER_READ_TIMEOUT=10
SERVER_WRITE_TIMEOUT=10
SERVER_MAX_HEADER_BYTES=1048576
SERVER_CTX_TIMEOUT=30
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0Search (additional variables)
# All PostgreSQL, Server, and Redis vars from above, plus:
KAFKA_LISTENERS=localhost:9092
ELASTICSEARCH_ADDR=http://localhost:9200GET /api/v1/management/events/:uuid
Returns full event details including venue, performers, and tickets with real-time reservation status.
| Status | Response |
|---|---|
| 200 | EventDTO |
| 404 | { "error": "...", "message": "Event not found" } |
| 500 | { "error": "...", "message": "Internal server error" } |
POST /api/v1/booking/tickets/reserve
Content-Type: application/json
{ "ticket_uuid": "string", "user_uuid": "string" }
| Status | Response |
|---|---|
| 200 | No content |
| 400 | { "error": "...", "message": "Invalid request payload" } |
| 409 | { "error": "...", "message": "Ticket is not available for reservation" } |
| 500 | { "error": "...", "message": "Internal server error" } |
POST /api/v1/booking/tickets/confirm
Content-Type: application/json
{ "ticket_uuid": "string", "user_uuid": "string", "payment_method_uuid": "string" }
| Status | Response |
|---|---|
| 200 | No content |
| 400 | { "error": "...", "message": "Invalid request payload" } |
| 409 | { "error": "...", "message": "Ticket is not reserved for this user" } |
| 500 | { "error": "...", "message": "Internal server error" } |
GET /api/v1/search/events?q={query}&page={page}&size={size}
| Param | Type | Default | Constraint |
|---|---|---|---|
| q | string | β | Search text |
| page | int | 1 | β₯ 1 |
| size | int | 10 | 1β100 |
| Status | Response |
|---|---|
| 200 | SearchEventsResponseDTO |
| 500 | { "error": "...", "message": "Internal server error" } |
ticketmaster/
βββ docker-compose.yml # Full infrastructure + services
βββ connector-config.json # Debezium PostgreSQL CDC connector config
βββ Makefile # Orchestration: migrations, CDC setup, Docker
βββ @docs/ # Architecture diagrams
β
βββ event-management/ # Event query service
β βββ cmd/server/main.go
β βββ internal/
β β βββ entities/ # Domain models (Event, Venue, Performer, Ticket)
β β βββ repositories/ # Data access layer (GORM)
β β βββ services/ # Event cache (Redis), Ticket lock check (Redis)
β β βββ usecases/ # FindEventByUUID
β β βββ handlers/ # HTTP handlers + DTOs
β β βββ server/ # Config, DB, Redis, Echo setup
β βββ migrations/ # Goose SQL migrations + seeds
β
βββ booking/ # Ticket reservation & confirmation service
β βββ cmd/server/main.go
β βββ internal/
β β βββ entities/ # Domain models
β β βββ repositories/ # Ticket repository (availability check, status update)
β β βββ services/ # Ticket lock (Redis distributed lock)
β β βββ usecases/ # ReserveTicket, ConfirmTicket
β β βββ handlers/ # HTTP handlers + DTOs
β β βββ server/ # Config, DB, Redis, Echo setup
β
βββ search/ # Full-text search service
βββ cmd/server/main.go
βββ internal/
β βββ brokers/cdc/ # Kafka CDC consumer (Debezium events + venues)
β βββ entities/ # Domain models
β βββ repositories/ # Venue repository (for location enrichment)
β βββ services/ # Elasticsearch engine, Search cache (Redis)
β βββ usecases/ # SearchEvents
β βββ handlers/ # HTTP handlers + DTOs
β βββ server/ # Config, DB, Redis, ES, Echo setup
