Runnable Spring Boot case studies for backend bugs that rarely show up in happy-path examples.
Each module keeps the broken baseline visible, adds one or more fix strategies, and ties the conclusion to tests, query diagnostics, execution plans, or manual benchmark evidence. The point is not to claim universal performance numbers. The point is to show how a backend problem was reproduced, diagnosed, fixed, and checked.
It is built for three readers:
- Backend engineers who want a concrete failure, diagnosis, and fix trade-off.
- Reviewers or interviewers who want evidence of backend judgment, not benchmark theater.
- Contributors who want to add a new scenario without breaking the measurement model.
The current modules focus on PostgreSQL-backed Spring systems: concurrency correctness, transaction deadlocks, ORM query shape, and deep pagination cost.
| Module | Failure mode | Strategies compared | Evidence |
|---|---|---|---|
| locking | booking race overbooks a shared resource | naive, pessimistic, optimistic, advisory lock | concurrency tests, benchmark results, pool pressure |
| deadlock | reversed account locks create circular waits | naive, ordered, timeout, advisory lock | integration tests, deadlock/timeout classification |
| n-plus-one | lazy loading turns list reads into many queries | naive, fetch join, entity graph, projection, batch size | query counts, PostgreSQL statistics, load test |
| pagination | deep offset scans and discards large row ranges | offset, cursor, deferred join | EXPLAIN ANALYZE, buffer reads, load test |
Start with locking if you want the shortest example of the repository pattern: reproduce the bug, prove the failure, compare fixes, and interpret the trade-off.
- Read one module README for the story and the engineering decision.
- Open that module's
docs/BENCHMARK_RESULTS.mdonly when you want the measured evidence. - Use MEASUREMENT.md to understand the evidence rules.
- Use docs/TRD.md when changing repository structure or adding a module.
Benchmark numbers are intentionally not repeated on this page. They drift by hardware, Docker resources, JVM warm-up, and PostgreSQL state. Each module owns its current measured values in its own result document.
git clone https://github.com/jaeyeopme/backend-patterns.git
cd backend-patterns
make build
make up-module MODULE=locking
curl http://localhost:8080/actuator/healthSupported module names: locking, deadlock, pagination, n-plus-one.
Stop the environment:
make downSeed endpoints require the benchmark profile. SEED=true starts the selected module with that profile.
make up-module MODULE=locking SEED=true
curl -X POST http://localhost:8080/admin/seed
curl -X POST "http://localhost:8080/bookings/optimistic?scheduleId=1&userId=42"Other examples:
make up-module MODULE=deadlock SEED=true
curl -X POST http://localhost:8081/admin/seed
curl -X POST "http://localhost:8081/transfers/ordered?fromId=1&toId=2&amount=100"
make up-module MODULE=pagination SEED=true
curl -X POST http://localhost:8082/admin/seed
curl "http://localhost:8082/articles/cursor?size=20"
make up-module MODULE=n-plus-one SEED=true
curl -X POST http://localhost:8083/admin/seed
curl "http://localhost:8083/teams/projection"make test # unit-tagged tests, no Docker
make verify # formatting + OpenRewrite dry-run + build, excluding integration tests
make test-all # unit + Docker-dependent integration testsUse the narrowest check that proves the change. Documentation-only changes usually need link, path, and terminology review rather than Java tests.
For a fresh proof run rather than Gradle cache reuse:
./gradlew spotlessCheck testUnit
./gradlew testUnit testIntegration --rerun-tasksThe forced integration run starts PostgreSQL Testcontainers and reruns the scenario tests instead of reporting UP-TO-DATE.
Use the narrowest evidence that proves the scenario. k6 is not a default proof surface for every module.
| Module | Primary evidence | Manual k6 role |
|---|---|---|
locking |
concurrent correctness tests and booking outcome classification | secondary/manual: contention, retry churn, and HikariCP pressure |
deadlock |
deterministic deadlock reproduction, timeout classification, and balance conservation | optional/manual appendix for stress behavior |
n-plus-one |
Hibernate Statistics and PostgreSQL pg_stat_statements query counts |
secondary/manual: throughput effect of query count and entity overhead |
pagination |
EXPLAIN / plan-shape evidence for scan/discard vs seek |
secondary/manual: concurrent-read amplification |
Manual load commands remain available when changing evidence or investigating runtime trade-offs:
SPRING_PROFILES_ACTIVE=docker,benchmark ./locking/run_benchmark.sh # secondary contention evidence
./n-plus-one/run_benchmark.sh # canonical secondary benchmark path
SPRING_PROFILES_ACTIVE=docker,benchmark ./pagination/run_benchmark.sh # secondary read-amplification evidence
SPRING_PROFILES_ACTIVE=docker,benchmark ./deadlock/run_benchmark.sh # optional stress appendixTreat benchmark output as a relative comparison inside the stated environment, not as an absolute throughput claim.
backend-patterns/
├── shared/ # common fixtures, exception handling, benchmark helpers
├── locking/ # overbooking and concurrency control
├── deadlock/ # transfer deadlocks and lock ordering
├── n-plus-one/ # JPA loading strategies
├── pagination/ # offset, cursor, and deferred-join pagination
├── scripts/ # benchmark result helpers
├── docs/ # PRD, TRD, ADRs
└── MEASUREMENT.md
Java 25, Spring Boot 4.0.3, PostgreSQL 17, Spring Data JPA, Hibernate, Testcontainers, k6, and Gradle Kotlin DSL.