02 / All work

Eventora

Event ticketing platform: one Spring Boot deployable (modular monolith) and a Next.js frontend.

Source (opens an external site)Frontend (opens an external site)Project site (opens an external site)

Event search results filtered by text, city, category and date, light theme

Problem

Many buyers reach for the same seats at the same moment. The booking path has to refuse oversell, absorb retried payments, and keep Redis and PostgreSQL in agreement.

Source · Event-Ticketing-Platform/README.md:7,159-164 · as of

Architecture

Scroll sideways for the full diagram.

  1. 01Client → BookingServicePOST /api/v1/bookings
  2. 02BookingService → Redisacquire per-user distributed lock
  3. 03BookingService → Redisre-check availability (TOCTOU guard)
  4. 04BookingService → RedisreserveSeat(): atomic Lua floor guard
  5. 05Redis → BookingServicedecremented, success
  6. 06BookingService → PostgreSQLatomic conditional UPDATE (availableCount -= n)
  7. 07BookingService → PostgreSQLINSERT Booking (state=RESERVED, expires=+5m)
  8. 08BookingService → Client201 Created
  9. 09BookingService → Redisrelease lock
Client
POST /api/v1/bookings
BookingService
Spring Boot API
Redis
lock, availability, Lua floor guard
PostgreSQL
conditional UPDATE, booking row
Source · Event-Ticketing-Platform/README.md:118-148 · as of
FIG. 01 / Reservation flow, concurrency-critical path. POST /api/v1/bookings; acquire per-user distributed lock; re-check availability (TOCTOU guard); reserveSeat(): atomic Lua floor guard; decremented, success; atomic conditional UPDATE (availableCount -= n); INSERT Booking (state=RESERVED, expires=+5m); 201 Created; rejected (floor guard); 409 Conflict; release lock

Key decisions

  1. 01

    A Redis Lua script checks availability and decrements in one atomic step. A test fires 100 concurrent threads at a 50-seat tier: exactly 50 succeed.

    Source · Event-Ticketing-Platform/README.md:159 · as of
  2. 02

    PostgreSQL inventory is written with one conditional UPDATE ... WHERE available_count >= :qty instead of read-modify-write.

    Source · Event-Ticketing-Platform/README.md:160 · as of
  3. 03

    Optimistic locking with @Version on Booking, TicketTier and Event; a lost update returns 409, not 500.

    Source · Event-Ticketing-Platform/README.md:162 · as of
  4. 04

    Stripe webhooks are deduplicated by a database UNIQUE constraint; booking creation requires an Idempotency-Key header.

    Source · Event-Ticketing-Platform/README.md:164,395 · as of
  5. 05

    QR codes and emails go through RabbitMQ with dead-letter queues, off the request thread.

    Source · Event-Ticketing-Platform/README.md:52,170 · as of
  6. 06

    JWT with a jti claim and a Redis denylist revokes tokens on logout; @PreAuthorize role checks gate 18 of 30 endpoints.

    Source · Event-Ticketing-Platform/README.md:392,396 · as of

Evidence

Commits2026-03-21 to 2026-09-07
415
Source · evidence-report.md:29 · as of
Automated tests228 / 228 passing
228
Source · Event-Ticketing-Platform/README.md:253 · as of
JaCoCo instruction coveragegated packages, gate 80%
84.1%
Source · Event-Ticketing-Platform/README.md:254 · as of
Controllers
9
Source · Event-Ticketing-Platform/README.md:55,240 · as of
Endpoints31 mapping annotations in code
30+
Source · Event-Ticketing-Platform/README.md:55; evidence-report.md:122 · as of
Flyway migrations
14
Source · Event-Ticketing-Platform/src/main/resources/db/migration · as of
Peak virtual usersRailway, ramp 10 to 200 VUs over 16 min, read path
200VUs
Source · Event-Ticketing-Platform/PERFORMANCE.md:263-298 · as of
RequestsRailway ramp, read path
32,577
Source · Event-Ticketing-Platform/PERFORMANCE.md:283 · as of
Failed requestsRailway ramp, read path
0.00%
Source · Event-Ticketing-Platform/PERFORMANCE.md:285 · as of
p95 latencyRailway ramp, about 394 ms
394ms
Source · Event-Ticketing-Platform/PERFORMANCE.md:288-291 · as of
Requests in five capacity runs, 0 failedlocal, Docker Compose; 0 server errors
569,066
Source · Event-Ticketing-Platform/PERFORMANCE.md:381-383,400-401; README.md:349 · as of
Read ceiling, one 1-CPU replicalocal, Docker Compose; read path, 0 errors
660req/s
Source · Event-Ticketing-Platform/PERFORMANCE.md:382,411 · as of
Median latency at that ceilinglocal, Docker Compose; p95 511 ms
2.40ms
Source · Event-Ticketing-Platform/PERFORMANCE.md:385-386 · as of
p95 at ceiling, 1 replicalocal, Docker Compose
511ms
Source · Event-Ticketing-Platform/PERFORMANCE.md:382,386 · as of
Read ceiling, 2 replicaslocal, Docker Compose; 0 errors
870req/s
Source · Event-Ticketing-Platform/PERFORMANCE.md:401 · as of
Scaling factor, 1 to 2 replicaslocal, Docker Compose; sub-linear
1.32×
Source · Event-Ticketing-Platform/PERFORMANCE.md:414 · as of
Sustained rate, 2 replicas behind nginxlocal, Docker Compose; p95 9.0 ms
800req/s
Source · Event-Ticketing-Platform/PERFORMANCE.md:400 · as of
p95 at 800 req/s, 2 replicaslocal, Docker Compose
9.0ms
Source · Event-Ticketing-Platform/PERFORMANCE.md:400,416 · as of
Oversold seats, 100-VU inventory burstlocal, Docker Compose; 8-seat tier
0
Source · Event-Ticketing-Platform/PERFORMANCE.md:321 · as of
Booking creation p95 under contentionlocal, Docker Compose; 20 VUs, 0 server errors
55.4ms
Source · Event-Ticketing-Platform/README.md:356; PERFORMANCE.md:320 · as of
020040060002505007501000throughput, req/sp95 latency, ms1 replica: 660 req/s · p95 511 ms. its ceiling: p95 crossed 500 ms here1 replica660 req/s · p95 511 ms2 replicas, nginx: 800 req/s · p95 9.0 ms. held every stage without abort2 replicas, nginx800 req/s · p95 9.0 ms
Read path: one replica vs two behind nginx
Configurationthroughput, req/sp95 latency, msNote
1 replica660511its ceiling: p95 crossed 500 ms here
2 replicas, nginx8009held every stage without abort

One replica's point is its ceiling; two replicas' point is a rate they held, not their ceiling (870 req/s, p95 568 ms).

Source · Event-Ticketing-Platform/PERFORMANCE.md:382,386,400-401,411-412,416 · as of
FIG. 02 / Read path: one replica vs two behind nginx (local, Docker Compose)
0501001502000246810121416minutestarget virtual users0 min: 0 target VUs2 min: 10 target VUs5 min: 25 target VUs8 min: 50 target VUs11 min: 100 target VUs14 min: 200 target VUs20016 min: 0 target VUs
Capacity ramp on Railway, 2026-07-04
minutestarget virtual users
00
210
525
850
11100
14200
160
requests
32,577
failed
0.00%
p95
394 ms

The line is the k6 stage schedule (configured targets), not measured VUs. Results are whole-run aggregates.

Source · Event-Ticketing-Platform/src/test/k6/capacity-ramp.js@d103b56:16-23; PERFORMANCE.md:271,283-291 · as of
FIG. 03 / Capacity ramp on Railway, 2026-07-04 (read path (browse and search))

Screenshots

Stack

  • Java 21
  • Spring Boot 3.5.13
  • Spring State Machine 4.0.0
  • PostgreSQL 17
  • Redis 7
  • RabbitMQ 4
  • Flyway
  • Stripe
  • Testcontainers
  • k6
  • Nginx 1.27
  • Next.js 16.2.4
  • React 19.2.4
  • Tailwind CSS 4
  • Vitest 3.2.4