Contention-safe reservation and selected-seat ticketing backend in Java 25.
resrv is a Spring Boot 4 and PostgreSQL backend for businesses that manage
scarce capacity: time slots, resources, and selected seats.
The interesting part is not CRUD. This repo focuses on the paths that usually break first in reservation systems:
- two customers trying to hold the same time range
- two customers trying to buy the same selected seat
- the same purchase confirmation being retried
- a generated slot becoming stale after schedule or policy changes
- business access changing after a JWT has already been issued
The result is one modular-monolith API with generated OpenAPI, PostgreSQL-backed contention control, account-scoped security, bounded-context package rules, and automated verification.
| Area | Current state |
|---|---|
| Purpose | Scarce-capacity API for reservation holds and selected-seat ticket purchases |
| Runtime | One supported Spring Boot API from the platform module |
| Language | Java 25 |
| Framework | Spring Boot 4, Spring MVC, Spring Security |
| Persistence | PostgreSQL 16, Flyway, Spring Data JPA |
| API contract | Generated OpenAPI from the running platform API |
| Modules | platform, timeslot, ticketing, platform-exchange, shared-kernel |
| Verification | Gradle check, Testcontainers, ArchUnit, JaCoCo, Checkstyle, Spotless, OpenRewrite |
| If you want to... | Read or run |
|---|---|
| Run the API locally | Quick start |
| Inspect endpoints and schemas | API contract |
| Understand the module split | Architecture |
| Review concurrency behavior | Correctness examples |
| Verify a change | Quality gates |
| Cut a public artifact | Releases and packages |
.
|-- .github/
| |-- pull_request_template.md
| `-- workflows/
| |-- ci.yml
| `-- release.yml
|-- config/
| `-- checkstyle/
|-- docs/
| |-- adr/
| |-- architecture.md
| |-- prd.md
| |-- security.md
| |-- testing.md
| `-- trd.md
|-- gradle/
| `-- libs.versions.toml
|-- platform/
| `-- src/
|-- platform-exchange/
| `-- src/
|-- shared-kernel/
| `-- src/
|-- ticketing/
| `-- src/
|-- timeslot/
| `-- src/
|-- AGENTS.md
|-- LICENSE
|-- README.md
|-- build.gradle.kts
|-- compose.yml
|-- settings.gradle.kts
`-- rewrite.yml
Generated Gradle outputs, local IDE state, local environment files, and local agent working directories are intentionally ignored.
The codebase is organized around a few backend guarantees:
- Gradle modules and package rules separate platform identity, booking, ticketing, exchange APIs, and shared primitives.
- JWTs identify only the account. Business access, reservation ownership, and ticket activity access are resolved server-side.
- Reservation holds use PostgreSQL advisory locks and active blocker checks instead of trusting previously generated availability.
- Ticket purchase confirmation uses all-or-nothing selected-seat ownership and customer-scoped idempotency replay.
- Generated OpenAPI is the endpoint and schema contract. The repository avoids a hand-written endpoint catalog.
- Tests cover domain behavior, application use cases, persistence, API flows, runtime wiring, architecture rules, and coverage thresholds.
Prerequisites:
- JDK 25.
- Docker for local PostgreSQL and Testcontainers-backed checks.
- Node 24 if you want commitlint and Lefthook installed locally.
Run the API locally:
./gradlew :platform:bootRunWhen no active Spring profile is set, bootRun uses the local profile. Start it
from the repository root so Spring Boot Docker Compose support can discover
compose.yml.
Open the generated API and health surfaces:
- Swagger UI: http://localhost:8080/swagger-ui.html
- OpenAPI JSON: http://localhost:8080/v3/api-docs
- OpenAPI YAML: http://localhost:8080/v3/api-docs.yaml
- Liveness: http://localhost:8080/actuator/health/liveness
- Readiness: http://localhost:8080/actuator/health/readiness
Install repository tooling when you plan to commit:
npm ci
npm run hooks:installRun the main verification path before opening a PR:
./gradlew spotlessApply
./gradlew rewriteDryRun
./gradlew check| Area | Implemented behavior |
|---|---|
| Identity | Account registration, login, account-scoped JWTs, inactive-account rejection |
| Account recovery | Repeated failed password sign-in protection and password reset challenge completion |
| Business access | Business creation, owner membership, staff grant/list/audit/update/disable |
| Booking setup | Booking settings, resources, weekly schedules, date override schedules |
| Public booking | Business discovery, active resource discovery, generated slots, authenticated hold creation |
| Reservations | Hold, confirm, release, customer cancel, business cancel, check-in, no-show, customer history, business search |
| Ticketing | Ticket events, sale windows, tiered inventory, selected seats, purchase confirmation, customer history, business activity |
| Runtime | One platform API serving platform, booking, and ticketing API groups |
Terminology:
- Booking means the broader scheduling workflow: settings, resources, schedules, generated slots, discovery, and hold creation.
- Reservation means the persisted time-range record and lifecycle facts: hold, confirm, release, cancel, check-in, and no-show.
The app runs as one Spring Boot process from the platform module. timeslot
and ticketing contribute booking and ticketing behavior to that process.
platform-exchange is a plain Java module for cross-context lookup and access
decisions. It is not HTTP, messaging, or an outbox layer.
The runtime path is simple:
- API clients call the
platformSpring Boot app. - The platform runtime assembles platform, booking, and ticketing API groups.
timeslotandticketingask platform-owned lookup and access questions throughplatform-exchange.- Each bounded context owns its PostgreSQL schema and migrations.
flowchart LR
client["API client"]
db[(PostgreSQL schemas)]
subgraph runtime["one platform Spring Boot process"]
app["platform API runtime"]
platform["platform accounts and access"]
timeslot["timeslot booking and reservations"]
ticketing["ticketing events and purchases"]
exchange["platform-exchange Java APIs"]
app --> platform
app --> timeslot
app --> ticketing
timeslot --> exchange
ticketing --> exchange
exchange --> platform
end
client --> app
platform --> db
timeslot --> db
ticketing --> db
Module roles:
| Module | Role |
|---|---|
shared-kernel |
Shared identity and timezone primitives |
platform-exchange |
Pure Java APIs used by other modules to ask platform for business and access decisions |
platform |
Accounts, login, businesses, memberships, security, migrations, and the runnable API |
timeslot |
Booking settings, resources, schedules, generated slots, and reservations |
ticketing |
Ticket events, inventory, selected seats, purchases, history, and business activity |
timeslot and ticketing do not depend on platform implementation packages or
platform persistence tables. They use platform-exchange for platform-owned
facts and access decisions. Their local bootRun and bootJar tasks remain
disabled, so there is one supported backend runtime.
More detail: docs/architecture.md and docs/trd.md.
Hold creation starts from a public business slug. The platform context resolves the active business, then timeslot validates the resource, slot identity, and capacity before saving a hold. The hold path revalidates generated slot identity against current settings and schedule data, then protects the resource/time range with a PostgreSQL advisory lock before checking active blockers.
The write path is:
- Resolve the active business from the public slug.
- Decode and revalidate the opaque
slotId. - Lock the resource and slot start in PostgreSQL.
- Check active blockers.
- Save the hold only when capacity is still available.
sequenceDiagram
autonumber
actor Customer
participant API as Public booking API
participant Platform as Platform context
participant Service as Reservation service
participant DB as PostgreSQL
Customer->>API: Request hold with business slug and slotId
API->>Platform: Resolve active business by slug
API->>Service: Create hold for account and resource
Service->>DB: Load resource policy and schedule
Service->>Service: Decode and revalidate slotId
Service->>DB: Take resource and slot advisory lock
Service->>DB: Check active blockers
alt Capacity blocked
Service-->>API: Reject as unavailable
else Capacity available
Service->>DB: Save hold with expiry
Service-->>API: Return held reservation
end
Reservation state is derived from timestamp facts on the reservation row. Held, confirmed, and checked-in reservations block capacity. Expired, released, cancelled, and no-show reservations do not.
Ticketing treats purchase confirmation as the first persisted ticket action:
- A successful confirmation creates one ticket purchase and marks every selected seat as purchased.
- Multi-seat confirmation is all-or-nothing.
- Contending customers cannot oversell a selected seat.
- Customer-scoped idempotency keys replay the original public outcome for 24 hours.
- Retries with changed details and retained expired keys return stable public problem reasons.
sequenceDiagram
autonumber
actor Customer
participant API as Ticketing API
participant Service as Purchase service
participant Idempotency as Idempotency store
participant Seats as Seat claim store
Customer->>API: Confirm purchase with idempotency key
API->>Service: Confirm selected seats
Service->>Idempotency: Create pending key or load existing outcome
alt Existing replayable outcome
Idempotency-->>Service: Original outcome
Service-->>API: Replay original response
else New confirmation
Service->>Seats: Lock seats in deterministic order
alt Every seat is available
Seats-->>Service: Claim all seats
Service->>Idempotency: Store purchased outcome
Service-->>API: Return confirmed purchase
else Any seat is unavailable
Service->>Idempotency: Store unavailable outcome
Service-->>API: Return unavailable seats conflict
end
end
The platform runtime generates the API contract:
- Swagger UI:
/swagger-ui.html - OpenAPI JSON:
/v3/api-docs - OpenAPI YAML:
/v3/api-docs.yaml
The repository does not maintain a hand-written docs/api.md endpoint catalog
or a committed OpenAPI snapshot. Narrative docs describe product scope,
architecture, security boundaries, and testing strategy. Exact paths, methods,
schemas, and response documentation come from generated OpenAPI.
Local build and runtime commands produce the files a reviewer needs:
| Evidence | How to generate | Where to inspect |
|---|---|---|
| OpenAPI contract | ./gradlew :platform:bootRun |
/v3/api-docs, /v3/api-docs.yaml, Swagger UI |
| Test reports | ./gradlew check |
*/build/reports/tests/test/index.html |
| Coverage reports | ./gradlew check |
*/build/reports/jacoco/test/html/index.html |
| Checkstyle reports | ./gradlew check |
*/build/reports/checkstyle/*.html |
| High-contention API behavior | ./gradlew :platform:test --tests '*Concurrency*' --tests '*HighContention*' |
Platform test report |
| Executable API jar | ./gradlew :platform:bootJar |
platform/build/libs/resrv-platform-api-0.0.1-SNAPSHOT.jar |
| Local container image | ./gradlew :platform:jibDockerBuild |
resrv-platform-api:latest |
The repository does not need extra snapshot files right now. Static OpenAPI snapshots, Postman collections, exported ERD images, and operations guides would duplicate generated OpenAPI, Flyway migrations, runtime health probes, or the existing design documents. Presentation and sales assets belong outside this repository.
Tagged releases use vMAJOR.MINOR.PATCH, for example v0.1.0.
When a matching tag is pushed, .github/workflows/release.yml:
- Runs OpenRewrite dry run and
check. - Builds the platform executable jar with the tag version.
- Creates a GitHub Release with the jar attached.
- Publishes the runnable API image to GitHub Container Registry as
ghcr.io/jaeyeopme/resrv-platform-api:MAJOR.MINOR.PATCH.
Manual releases can also be started from the GitHub Actions tab with the same
version format. Java module jars are not published to Maven packages yet because
only the platform API runtime is a supported public artifact.
Architecture detail lives in docs and ADRs:
| Reference | Location |
|---|---|
| Runtime and bounded-context map | docs/architecture.md |
| Module dependency map | docs/architecture.md |
| Persistence ownership map | docs/architecture.md |
| Contention correctness catalog | docs/architecture.md |
The primary check sequence is:
./gradlew spotlessApply
./gradlew rewriteDryRun
./gradlew checkcheck covers compilation, tests, Checkstyle, ArchUnit tests, JaCoCo report
generation, and coverage verification. CI also runs commitlint and CodeQL.
Focused checks:
./gradlew :platform:test --tests '*Ticketing*'
./gradlew :platform:test --tests '*Concurrency*' --tests '*HighContention*'
./gradlew :platform:test --tests io.resrv.platform.api.PlatformRuntimePackagingIntegrationTest
./gradlew :platform:test --tests io.resrv.platform.api.PlatformOperationalReadinessIntegrationTest| Document | Purpose |
|---|---|
| docs/prd.md | Product scope, concepts, flows, acceptance criteria, and open product questions |
| docs/trd.md | Current technical design, runtime, persistence, security, and configuration reference |
| docs/architecture.md | Bounded contexts, module boundaries, and contention correctness patterns |
| docs/security.md | Authentication, authorization, public exposure, data boundaries, and deferred hardening |
| docs/testing.md | Test strategy, quality gates, coverage thresholds, and focused verification commands |
| docs/adr/README.md | Architecture decision record index |
This project is licensed under the MIT License.
Out of scope now:
- Load benchmarking, traffic simulation, and production capacity planning.
- Payments, deposits, invoices, and refunds.
- Staff invitation delivery and acceptance UI.
- Password reset UI. Backend challenge completion exists.
- Notifications and reminders, except SMTP-compatible password reset delivery.
- External calendar sync.
- Separate
timeslotorticketingruntimes. - Message broker, outbox, event projections, and production runtime splitting.
- Ticketing checkout attempts, failed-attempt persistence, holds, cancellations, expiration records, waitlists, resale, public marketing discovery, and seating map editing.