Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

LoadLab — Break a Spring Boot Service on Purpose

A Spring Boot service that fails on demand in eight documented ways, with a k6 harness, JFR and heap-dump tooling wired in, so you can debug a real memory leak and a real CPU spike instead of reading about them.

Advanced~20 hoursUpdated

Stack at a glance

Service
Spring Boot 3Java 21HikariCPCaffeineResilience4j
Data
PostgreSQLRedisFlywayTestcontainers
Load
k6GatlingJMH
Diagnostics
JDK Flight Recorderasync-profilerEclipse MATjcmdPrometheusGrafana
On this page

Almost every interview asks about a production issue you have debugged, and most developers have only ever read about memory leaks and CPU spikes. LoadLab exists to fix that: a service that fails on demand, in eight specific documented ways, with the real tooling wired in so you diagnose each one with jcmd, JFR and MAT rather than with a tutorial.

Key Takeaways

  • Each scenario reproduces a failure from Phase 6 with realistic symptoms.
  • The load harness uses an open arrival-rate model, so the tail latency numbers are honest.
  • Blind mode enables a random scenario, which is the only way to practise diagnosis rather than confirmation.
  • The point is not the code — it is being able to say "I have seen this, here is what I ran".
  • Budget about 20 hours: two to three per scenario, plus setup.

What you build

A small order service — create, look up, list, enrich — with a Postgres database, a Redis cache and a stubbed downstream dependency whose latency you control. On top of that, a configuration layer that enables one deliberate defect at a time.

One service, one switchable defect at a time, with load generation and diagnostics both wired in from the start.
getting started
git clone https://github.com/javaagentic/loadlab && cd loadlab
docker compose up -d              # postgres, redis, prometheus, grafana
./mvnw spring-boot:run            # the service, with JFR already recording
 
# In another terminal — steady baseline load
k6 run harness/baseline.js
 
# Enable a scenario
curl -X POST localhost:8080/lab/scenario/memory-leak/enable

The eight scenarios

#ScenarioSymptom you will seeThe tool that finds it
1Static-collection leakHeap floor rises after each Full GCjstat, heap dump, MAT dominator tree
2CPU spike (regex backtracking)One core pinned, latency climbingtop -H, nid mapping, thread dump
3Connection-pool exhaustionAll requests time out, CPU idleHikari metrics, leakDetectionThreshold
4Thread-pool starvationQueue depth unbounded, then OOMThread dump: N identical stacks
5N+1 querySlow endpoint, fast databaseHibernate statistics, query count
6GC thrashingThroughput collapse, GC threads at 100%GC log, allocation profile
7Cache stampedePeriodic database saturation spikesHit-ratio metric, query-rate spike
8Retry stormOne dependency slows, everything failsTraces, dependency call multiplication

Each maps directly onto a tutorial in Phase 6, so you can read the theory, reproduce the failure, and then compare what you found against what the page said you would find.

How a session goes

scenario 1 — the memory leak
# 1. Baseline for five minutes, note the heap floor after each Full GC
k6 run harness/baseline.js &
jstat -gcutil $(pgrep -f loadlab) 5000
 
# 2. Enable the leak
curl -X POST localhost:8080/lab/scenario/memory-leak/enable
 
# 3. Watch the floor climb. This is the signature — not "the heap is full",
#    but "occupancy after a full collection keeps rising".
 
# 4. Capture the evidence BEFORE it dies
jcmd $(pgrep -f loadlab) GC.heap_dump /tmp/leak.hprof
 
# 5. Open in Eclipse MAT: Leak Suspects, then the dominator tree,
#    then Path to GC Roots with weak references excluded.
 
# 6. Compare what you found against
#    /learn/jvm-memory-performance/java-memory-leak-patterns

The runbook for each scenario gives you the symptom and the commands but not the cause — that is in a separate answers file, so the exercise is diagnosis rather than reading.

Blind mode

the version that actually prepares you
curl -X POST localhost:8080/lab/scenario/random/enable
# Enables one of the eight, without telling you which.

Knowing which failure you are looking for makes the diagnosis trivial and teaches you very little. Blind mode forces the real skill: start from the symptom, use RED to find the unhealthy component, use USE to find the exhausted resource, and narrow down from there — the sequence in the incident playbook.

Give yourself fifteen minutes per attempt and write down what you concluded before checking. Being wrong is the useful part.

Measuring honestly

harness/baseline.js — an open model
export const options = {
  scenarios: {
    steady: {
      executor: 'constant-arrival-rate',   // NOT constant-vus
      rate: 200, timeUnit: '1s',
      duration: '30m',
      preAllocatedVUs: 200, maxVUs: 2000,
    },
  },
  thresholds: {
    http_req_duration: ['p(99)<500'],
    dropped_iterations: ['count<1'],       // the generator kept the schedule
  },
};

The arrival-rate executor is deliberate. A closed model would slow its own request rate whenever the service slowed, hiding the exact behaviour the lab exists to demonstrate — that is coordinated omission, explained in Latency spikes. The dropped_iterations threshold fails the run if the generator itself could not keep up, which keeps the numbers trustworthy.

Extending it

Once the eight scenarios are familiar, the useful additions are the ones that mirror what you would actually build at work:

Fix each one properly, then re-run the same load and confirm the metric moved. A fix you have not measured is a hypothesis.

Add the guardrail. For each scenario, add the alert that would have caught it: pool pending count, executor queue depth, cache hit ratio, GC time percentage. Then re-enable the scenario and check the alert fires before the failure does.

Add a scenario of your own from an incident you have actually seen. Reproducing your own war story is the best possible preparation for telling it.

Wire in distributed tracing and add a second service, so the retry-storm scenario propagates the way it does in a real estate.

Why this is worth twenty hours

The interview answer changes completely. Instead of "I know that a memory leak means objects are retained", you can say: "I have taken a heap dump on a service leaking about 200MB an hour, opened it in MAT, found the accumulation point in the dominator tree, and traced it back through path-to-GC-roots to a static map with no eviction. The tell was that heap occupancy after each Full GC kept climbing rather than returning to the same floor."

The second answer is specific, uses the right vocabulary, and describes a process. That is the difference the lab buys, and it is why this project exists.