advanced~2.5h

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
CulpritHow it shows upWhere it's covered
N+1 queriesResponse time scales linearly with result-list sizeModule 09
Missing database indexOne specific query is slow regardless of app-server loadDatabases Mastery — SQL
Connection pool exhaustionIntermittent slowness ONLY under concurrent loadThis module, next section
Synchronous external API callsOne endpoint is slow exactly as long as a downstream call takesModule 25 — Async
No caching on a hot read pathThe same expensive computation repeats identically every requestModule 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 →

Practice quiz

Next Step

Continue to Production Best Practices← Back to all Spring Boot chapters