Performance Optimization
A slow endpoint is rarely slow for one obvious reason — it's usually three or four smaller ones stacked together. This module is how to find them systematically instead of guessing.
Learning objectives
- Beginner: Use Actuator metrics to identify which endpoint is actually slow before trying to fix anything.
- Intermediate: Recognize connection-pool exhaustion as a cause of intermittent slowness under load.
- Advanced: Design a load test that reproduces a production performance issue locally before attempting a fix.
◆ The problem
Optimizing the first thing that LOOKS slow, without measuring, routinely wastes days fixing something that was never the actual bottleneck — the real slow path is often somewhere unglamorous, like a synchronous external API call buried three layers deep.
Module 27's Actuator /actuator/metrics endpoint already exposes per-endpoint response time data (http.server.requests) — the correct first step for ANY performance investigation is checking what that data actually says, not guessing.
GET /actuator/metrics/http.server.requests?tag=uri:/api/orders
| Culprit | How it shows up | Where it's covered |
|---|---|---|
| N+1 queries | Response time scales linearly with result-list size | Module 09 |
| Missing database index | One specific query is slow regardless of app-server load | Databases Mastery — SQL |
| Connection pool exhaustion | Intermittent slowness ONLY under concurrent load | This module, next section |
| Synchronous external API calls | One endpoint is slow exactly as long as a downstream call takes | Module 25 — Async |
| No caching on a hot read path | The same expensive computation repeats identically every request | Module 17 — Redis Caching |
Most real production slowness is one of these five, not something exotic.
⚠ Common real-world trap
A connection pool sized too small for real concurrent load causes a very specific symptom: everything is fast under light traffic, then intermittently and unpredictably slow the moment concurrent requests exceed the pool size — because extra requests simply WAIT for a connection to free up rather than failing outright.
spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.connection-timeout=5000
Sizing this correctly requires knowing your actual concurrent request volume and how long each database call holds a connection — a pool that's too large just exhausts the DATABASE's own connection limit instead, moving the same problem one layer over.
A performance bug that only appears under real production concurrency is nearly impossible to fix by reading code alone — a load-testing tool (k6, Gatling, or even a simple script firing concurrent requests) that reproduces the SAME concurrency pattern locally turns a guessing exercise into an actual measurable before/after comparison.
✓ Quick recap
- Measure with real data (Actuator metrics) before touching code — don't optimize by guessing.
- N+1 queries, missing indexes, connection pool exhaustion, synchronous external calls, and missing caching cover most real-world slowness.
- Connection pool exhaustion's signature symptom is intermittent slowness that appears ONLY under concurrent load, not constant slowness.
- Reproduce the issue locally with a load test before attempting a fix, so you can actually measure whether the fix worked.
Want a visual for this concept?
Generate a diagram tailored to “Performance Optimization” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →