TestForge Blog
← All Posts

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

  1. DB query optimization (N+1, indexes) — highest impact
  2. Connection pool tuning — immediate effect
  3. Caching — eliminate repeated lookups
  4. Async processing — reduce response latency
  5. Response compression — reduce network cost
  6. JVM tuning — fine-tuning last

Most performance problems originate in the DB layer.
Apply steps 1–5 first; JVM tuning is the final step.