Spring Boot Performance Tuning — Cut Response Time by 50%
Practical tuning methods to reduce Spring Boot application response time. Step-by-step guide covering DB connection pools, JPA optimization, caching, and JVM settings.
TestForge Team ·
Measure First
Always establish a baseline before tuning. Numbers, not intuition.
# Enable Spring Boot Actuator
management.endpoints.web.exposure.include=health,metrics,prometheus
# Load test with k6
k6 run --vus 50 --duration 60s script.js
1. DB Connection Pool Optimization (HikariCP)
Spring Boot’s default connection pool is HikariCP. The defaults are surprisingly small.
spring:
datasource:
hikari:
maximum-pool-size: 20 # Default 10 → set to CPU cores × 2–4
minimum-idle: 5
connection-timeout: 3000 # Exception if no connection acquired within 3s
idle-timeout: 600000 # Return idle connections after 10 minutes
max-lifetime: 1800000 # 30-minute max connection lifetime
leak-detection-threshold: 5000 # Warn if connection unreturned after 5s
Warning: Blindly increasing maximum-pool-size can overload the database.
Keep it below 80% of the DB server’s max_connections.
2. Eliminate JPA N+1 Problems
The single most common cause of performance degradation.
// Problem: 1 query for Orders, then N queries for items
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size()); // N+1!
// Fix 1: Fetch Join
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.userId = :userId")
List<Order> findWithItems(@Param("userId") Long userId);
// Fix 2: @EntityGraph
@EntityGraph(attributePaths = {"items", "user"})
List<Order> findByStatus(String status);
// Fix 3: Batch Size (bundle collections into IN clause)
@BatchSize(size = 100)
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
3. Add a Cache Layer
// Spring Cache + Redis
@EnableCaching
@Configuration
public class CacheConfig {
@Bean
public RedisCacheConfiguration cacheConfig() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
}
}
// Usage
@Cacheable(value = "product", key = "#id")
public ProductDto getProduct(Long id) {
return productRepository.findById(id)
.map(ProductDto::from)
.orElseThrow();
}
@CacheEvict(value = "product", key = "#id")
public void updateProduct(Long id, UpdateRequest req) { ... }
4. Async Processing
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
// Offload non-critical work (email, notifications) to async
@Async
public CompletableFuture<Void> sendNotification(Long userId, String message) {
notificationService.send(userId, message);
return CompletableFuture.completedFuture(null);
}
5. Response Compression
server:
compression:
enabled: true
mime-types: application/json,application/xml,text/html
min-response-size: 1024 # Only compress payloads >= 1KB
Typical result: 40–70% reduction in JSON payload size.
6. JVM Tuning
java -jar app.jar \
-Xms512m -Xmx1g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=16m \
-XX:+ParallelRefProcEnabled \
-XX:+DisableExplicitGC
7. Automatic Slow Query Detection
spring:
jpa:
properties:
hibernate:
generate_statistics: true
session.events.log.LOG_QUERIES_SLOWER_THAN_MS: 100 # Log queries > 100ms
logging:
level:
org.hibernate.stat: DEBUG
Tuning Priority Order
- DB query optimization (N+1, indexes) — highest impact
- Connection pool tuning — immediate effect
- Caching — eliminate repeated lookups
- Async processing — reduce response latency
- Response compression — reduce network cost
- JVM tuning — fine-tuning last
Most performance problems originate in the DB layer.
Apply steps 1–5 first; JVM tuning is the final step.