Performance · Hub

Java Performance Engineering: From JVM to AWS

A practical, measurement-driven guide to diagnosing, optimising and scaling Java and Spring Boot services. Every page is a lab: observe a symptom, form a hypothesis, measure, diagnose, change one thing, and prove the fix with numbers. This page is the control room — start from the symptom you are seeing in production.

15
Lab Pages
4
Layers
21
Java Version
01
Section One · Philosophy

The Loop: Measure, Find, Fix, Prove

Most performance advice fails for one reason: it hands you a setting without telling you how to know whether the setting helps. Raising a heap, switching a garbage collector or doubling a pool are all hypotheses. Engineering is the discipline of testing them. Every page in this guide repeats one loop.

The performance engineering loop
1 MEASURE baseline, p50/p99 2 FIND the bottleneck 3 FIX ONE variable 4 PROVE rerun, compare the new number becomes the next baseline
📏

Understand

How Java, Spring, databases and AWS behave under load — the mechanisms, not just the knobs.

🔬

Diagnose

Find bottlenecks with profilers, metrics, traces and load tests instead of guessing.

✅

Optimise & verify

Make a measurable improvement and validate it under a realistic workload.

02
Section Two · The Stack

Where the Time Actually Goes

A slow API is rarely slow in one place. A single request crosses the client, a load balancer, a container, the JVM, Spring, a connection pool, the network and a database — and a problem at any layer looks like “the service is slow” from outside. The first skill is attributing the latency to a layer before touching anything.

One request, eight places to wait
Client DNS · TLS ALB idle timeout Container CPU throttle JVM GC · JIT Spring threads · Jackson Pool Hikari DB SQL · locks Latency budget of an 800 ms request illustrative example, not a measurement app 30 database query 650 ms (81%) serialise 20 other 100 Tuning the app code (30 ms) or the JSON library (20 ms) cannot fix this request. Amdahl: the biggest slice caps the gain — so find it first (page 01, 05, 08).
Rule of thumb. Before any change, be able to say: “this request spends X ms in layer Y, measured by tool Z.” If you cannot, you are not tuning yet — you are guessing.
03
Section Three · Control Room

Symptom → Likely Cause → Tool → Fix

This is the table to bookmark. Find what you are seeing in production, check the likely causes in order, run the listed tool to confirm one, and follow the link to the lab page that teaches the fix with measurements. Several causes can overlap — the tool column is how you tell them apart.

SymptomLikely causesFirst toolGo to
High CPUHot loop or regex, excessive serialisation, GC threads, lock spinning, JIT compiling after a deployJFR / async-profiler CPU profile, top -H05 Profiling, 04 JIT
High p99, fine p50GC pauses, lock contention, slow downstream tail, retries, CPU throttling, connection-pool waitsLatency histogram + distributed trace of slow requests01 Measure, 10 Distributed
Latency spikes at a fixed interval (e.g. every 30 s)Periodic GC, scheduled job, cache TTL expiry stampede, connection churn, container CPU throttlingGC log timeline, JFR, correlate with scheduler/TTL03 GC, 09 Caching
Memory keeps growingLeak (static map, listener, ThreadLocal), unbounded cache or queue, native/direct memoryHeap histogram, heap dump + dominator tree03 Memory, 05 Profiling
Frequent or long GCHigh allocation rate, undersized heap, wrong collector for the goal, humongous objectsGC logs (-Xlog:gc*), JFR allocation profile03 GC
Pod OOMKilled, heap looks half fullContainer limit is heap plus metaspace, threads, direct buffers, code cacheContainer events, NMT (-XX:NativeMemoryTracking)12 AWS Compute
Threads blocked / requests queue upLock contention, exhausted pool, blocking call on an event loop, pinned virtual threadsThread dumps (jcmd Thread.print)06 Concurrency
Connection is not available, request timed outPool too small or leaked connections, slow queries holding connections, long transactionsHikari metrics (pending/acquire), thread dump08 Database
Database latency spikesMissing index, bad plan, N+1, lock waits, connection stormQuery plan (EXPLAIN ANALYZE), slow-query log, DB insights08 Database
Throughput stops rising when adding load or instancesSaturated shared resource (DB, lock, pool), serial fraction (Amdahl/USL), client is the limitLoad test with step ramp, USE method per resource11 Capacity, 02 Load tests
Slow right after deploy or scale-outCold JIT, empty caches, class loading, readiness probe too earlyWarm-up curve, startup logs, CDS/AOT comparison04 JIT & Startup, 13 Scaling
502 / 504 from the load balancerKeep-alive or idle-timeout mismatch, upstream timeouts, unhealthy targets, saturationALB access logs, target response time vs request time07 Network
One slow dependency drags everythingNo timeout, retry storm, no bulkhead, fan-out tail amplificationTrace waterfall, per-dependency latency and error rate10 Distributed
Queue lag growingConsumers slower than producers, poison message retries, small batches, too few partitionsConsumer lag / queue-depth metric, consumer profile10 Messaging
Cold starts hurt (Lambda / scale-to-zero)JVM startup, class loading, framework init, small memory (and thus CPU) settingInit duration in logs, startup timeline04 Native, 12 Lambda
AWS bill keeps risingOver-provisioned instances, idle headroom, scaling on the wrong signal, DB tier too big, no cachingCost Explorer, cost per request, utilisation vs requests14 Cost
Do not skip the “confirm” step. “High p99” has at least six plausible causes. Changing the garbage collector because it is the most famous suspect, when the real cause is a pool wait, costs a week and proves nothing. Rule out causes with evidence, cheapest test first.
04
Section Four · The Lab

OrderService: One Slow System, Fifteen Labs

Rather than fifteen disconnected toy examples, every page works on a slice of one recurring system: OrderService, a Spring Boot 3 / Java 21 REST service on AWS. It has been written to be realistically bad — the kind of service that works fine in a demo and falls over in production.

OrderService topology (the lab system)
Clients ALB ECS Fargate / EKS pods OrderService OrderService JVM · Tomcat · Hikari Aurora / RDS ElastiCache Payment API Inventory API SQS dashed = not used yet in the baseline (added by later labs)
Built-in flawWhat it causesFixed in
N+1 queries when listing a customer’s ordersDozens of round trips per request08 Database
No index on orders.customer_idSequential scans that grow with the table08 Database
Connection pool far too small (or far too big)Pool waits; or a database drowning in connections08, 11
No caching of product and customer lookupsRepeated identical queries09 Caching
Payment and Inventory called one after the otherLatencies add; one slow call stalls everything06, 10
New HTTP connection per downstream call, no timeoutsHandshake cost; hangs under failure07 Network
Default JVM flags in a 2-vCPU, 4 GB containerOdd heap size, GC choice, throttling03, 12
Oversized entity JSON, no compressionCPU on serialisation, large payloads07 Spring
05
Section Five · Method

The Six-Step Lab Format

Every deep chapter in this guide uses the same six steps, so you can apply the same structure to your own production incidents.

1 · Problem — what are we observing, with a number? (“p99 is 800 ms, target 200 ms.”)
2 · Hypothesis — what might cause it? Write it down before measuring.
3 · Measurement — which tool and which metric, with the exact command.
4 · Diagnosis — what did the data show? Was the hypothesis right?
5 · Optimisation — change one major variable.
6 · Verification — rerun the identical test; compare before and after.
MetricWhy we always record it
Average, p95, p99 latencyThe average hides the tail; the tail is what users and SLOs feel
Throughput (requests/s)A latency win that halves throughput is not a win
CPU utilisationShows whether you freed capacity or just shifted the bottleneck
Resource-specific countersGC pause, pool pending, queries per request — the metric your change targets
One variable at a time. If you change the pool size, the collector and add a cache in one release and latency improves, you have learned nothing you can reuse. The capstone page applies every optimisation sequentially and shows the cumulative effect so you can see which change paid for itself.
06
Section Six · Learning Path

The 15 Labs, in Four Groups

Read in order the first time — the groups build on each other. Afterwards, use the symptom map above as your entry point.

A · Foundations

B · The JVM

C · Spring & the Data Tier

D · AWS & Production

07
Section Seven · Ground Rules

How to Read the Numbers

Measured here

Experiments marked real output were run on a 12-core Linux machine with Java 21 and the results pasted in. They show the shape and the method. Your absolute numbers will differ with hardware, JVM build and load — that is exactly why you should rerun them.

Illustrative

Anything that needs real AWS services (Aurora, Lambda cold starts, CloudWatch, Fargate throttling, prices) is labelled illustrative. Cost figures use example list prices — always check current AWS pricing for your region.

Versions matter

The guide targets Java 21 LTS and Spring Boot 3.x. Where behaviour differs by JDK (virtual-thread pinning, generational ZGC, preview APIs) the page states the version.

No magic flags

You will not find a “fast JVM flags” list. A flag is only presented next to the evidence that motivates it and the measurement that shows its effect.

∑ Performance engineering - what to remember
  • Performance work is a loop: measure, find the bottleneck, change one thing, prove it.
  • A slow request is attributed to a layer first — the biggest slice caps what any fix can achieve.
  • The symptom map is the entry point; the labs teach the fixes with measurements.
  • Start at 01 · Measure First.