Skip to content

jaeyeopme/backend-patterns

Repository files navigation

Backend Bug Fix Patterns

CI

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.

Case studies

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.

Reading path

  1. Read one module README for the story and the engineering decision.
  2. Open that module's docs/BENCHMARK_RESULTS.md only when you want the measured evidence.
  3. Use MEASUREMENT.md to understand the evidence rules.
  4. 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.

Quick start

git clone https://github.com/jaeyeopme/backend-patterns.git
cd backend-patterns

make build
make up-module MODULE=locking
curl http://localhost:8080/actuator/health

Supported module names: locking, deadlock, pagination, n-plus-one.

Stop the environment:

make down

Try a strategy

Seed 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"

Verification

make test      # unit-tagged tests, no Docker
make verify    # formatting + OpenRewrite dry-run + build, excluding integration tests
make test-all  # unit + Docker-dependent integration tests

Use 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-tasks

The forced integration run starts PostgreSQL Testcontainers and reruns the scenario tests instead of reporting UP-TO-DATE.

Evidence and manual load checks

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 appendix

Treat benchmark output as a relative comparison inside the stated environment, not as an absolute throughput claim.

Repository layout

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

Technology stack

Java 25, Spring Boot 4.0.3, PostgreSQL 17, Spring Data JPA, Hibernate, Testcontainers, k6, and Gradle Kotlin DSL.

About

Reproducible Spring Boot/JPA/PostgreSQL failure patterns with tests, diagnostics, and benchmark evidence.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors