Home / Spring Boot / Production and Testing / Performance Tuning: A Practical Method

Performance Tuning: A Practical Method

Guessing is the enemy of performance work. This page gives you a method (measure, find the bottleneck, fix, measure again) and the checklist of typical bottlenecks in a Spring Boot service, from database to JVM.

Method

1. Define the goal      "p95 latency < 300 ms at 500 requests per second"
2. Reproduce it        load test (k6, Gatling, JMeter) with realistic data and traffic
3. Measure             metrics + traces + profiler: WHERE is the time? (DB? CPU? waiting? GC?)
4. Fix ONE thing       the biggest bottleneck only
5. Measure again       did it help? if not, revert
Typical distribution in business apps:  database 60%  ─  remote calls 20%  ─  serialization/CPU 10%  ─  JVM/GC 10%

Database first

# log statements slower than 25 ms (Hibernate 6)
spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=25
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
spring.jpa.open-in-view=false
// Keyset pagination: fast for deep pages (uses the index, no OFFSET scan)
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("select o from Order o where o.id < :lastId order by o.id desc")
    List<Order> nextPage(@Param("lastId") long lastId, Pageable limit);     // PageRequest.of(0, 50)
}
// client sends the last id it saw: GET /orders?after=12345&size=50

Application layer

ProblemFix
Repeated identical readsCache (Caffeine or Redis) with TTL and eviction
Sequential remote callsRun independent calls in parallel (CompletableFuture, structured concurrency, WebClient zip)
Slow serialization of huge JSONSmaller DTOs, pagination, compression, streaming (StreamingResponseBody)
Thread pool exhaustionTimeouts on every client, bulkheads, virtual threads (Java 21+)
Chatty APIsBatch endpoints, GraphQL or composite endpoints
Heavy work in the requestMove to async processing (queue) and return 202 Accepted

JVM and containers

java -XX:MaxRAMPercentage=75 \
     -XX:+UseG1GC \                        # default, good balance. Try -XX:+UseZGC (generational in newer JDKs) for very low pauses
     -Xlog:gc*:file=gc.log:time,uptime \
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp \
     -XX:StartFlightRecording=filename=rec.jfr,duration=120s,settings=profile \
     -jar app.jar

HTTP and network

Startup and memory footprint

Load test example (k6)

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = { stages: [
  { duration: '30s', target: 50 },
  { duration: '2m',  target: 200 },
  { duration: '30s', target: 0 } ],
  thresholds: { http_req_duration: ['p(95)<300'], http_req_failed: ['rate<0.01'] } };

export default function () {
  const res = http.get('http://localhost:8080/api/products?page=0&size=20');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}

Common mistake: Optimizing without measuring, for example adding caches or threads everywhere. You add complexity and bugs and the real bottleneck (often one missing index or an N+1 query) stays. Always find the bottleneck with data first.

Interview questions

What is the first thing to check when an endpoint is slow?

The database: number of queries per request (N+1), slow queries and missing indexes. A trace or SQL log shows this quickly.

Should I increase the connection pool size to go faster?

Rarely. A bigger pool can overload the database. First remove slow queries and long transactions.

p50 or p99?

Look at percentiles. p50 is the typical user, p95 and p99 show the slow tail that hurts real users and reveals problems like GC pauses and pool waits.