Skip to main content

On This Page

Spring Boot Performance Optimization: Expert Tips and Techniques

8 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Spring Boot Performance Optimization: Expert Tips and Techniques

Performance is critical for production Spring Boot applications. This comprehensive guide covers everything from JVM tuning to database optimization, helping you build blazing-fast Spring Boot applications that scale.

Table of Contents

  1. Performance Metrics That Matter
  2. JVM and Memory Optimization
  3. Database Performance
  4. Caching Strategies
  5. Connection Pool Tuning
  6. Async Processing
  7. Monitoring and Profiling
  8. Real-World Optimizations

Performance Metrics That Matter {#metrics}

Before optimizing, understand what to measure:

Key Metrics:

  • Response Time: p50, p95, p99 percentiles
  • Throughput: Requests per second
  • Error Rate: % of failed requests
  • CPU Usage: Application CPU consumption
  • Memory Usage: Heap and non-heap memory
  • GC Pause Time: Garbage collection impact
  • Database Query Time: Query execution duration

JVM and Memory Optimization {#jvm-optimization}

1. Choosing the Right JVM

Recommended JVMs:

  • OpenJDK (HotSpot): Default, well-tested
  • GraalVM: Faster startup, lower memory footprint
  • Azul Zing: Ultra-low latency applications

2. Heap Size Configuration

# Production JVM Settings
java -Xms2G -Xmx2G \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/logs/heapdump.hprof \
     -XX:+ExitOnOutOfMemoryError \
     -jar application.jar

Key Parameters:

  • -Xms: Initial heap size
  • -Xmx: Maximum heap size
  • Best Practice: Set Xms = Xmx to avoid heap resizing

3. Garbage Collection Tuning

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:ConcGCThreads=4
-XX:ParallelGCThreads=8

ZGC (For low-latency applications with large heaps)

-XX:+UseZGC
-XX:+ZGenerational
-XX:ZCollectionInterval=120

4. Monitor GC Activity

@Component
public class GCMetricsExporter {
    
    @Scheduled(fixedRate = 60000)
    public void exportGCMetrics() {
        List<GarbageCollectorMXBean> gcBeans = 
            ManagementFactory.getGarbageCollectorMXBeans();
        
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            log.info("GC Name: {}, Collections: {}, Time: {}ms",
                gcBean.getName(),
                gcBean.getCollectionCount(),
                gcBean.getCollectionTime());
        }
    }
}

Database Performance {#database-performance}

1. Connection Pool Optimization

HikariCP Configuration (Spring Boot Default):

spring:
  datasource:
    hikari:
      # Core settings
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
      # Performance settings
      leak-detection-threshold: 60000
      connection-test-query: SELECT 1
      validation-timeout: 5000
      
      # Advanced settings
      auto-commit: false
      register-mbeans: true

Optimal Pool Size Formula:

pool_size = (core_count * 2) + effective_spindle_count

For example, with 4 cores and 1 disk: (4 * 2) + 1 = 9

2. JPA/Hibernate Optimization

Enable Second-Level Cache

@Entity
@Cacheable
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE,
    region = "productCache"
)
public class Product {
    @Id
    private Long id;
    private String name;
    private BigDecimal price;
}
spring:
  jpa:
    properties:
      hibernate:
        # Enable second-level cache
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
        
        # Connection handling
        connection:
          provider_disables_autocommit: true
        
        # Batching
        jdbc:
          batch_size: 20
          fetch_size: 50
        order_inserts: true
        order_updates: true
        
        # Query optimization
        query:
          in_clause_parameter_padding: true
          plan_cache_max_size: 2048
          
        # Statistics (disable in prod)
        generate_statistics: false

Batch Operations

@Service
public class ProductService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Transactional
    public void batchInsert(List<Product> products) {
        int batchSize = 20;
        
        for (int i = 0; i < products.size(); i++) {
            entityManager.persist(products.get(i));
            
            if (i > 0 && i % batchSize == 0) {
                // Flush and clear the persistence context
                entityManager.flush();
                entityManager.clear();
            }
        }
    }
}

Fetch Strategy Optimization

@Entity
public class Order {
    
    @Id
    private Long id;
    
    // Use JOIN FETCH to avoid N+1 queries
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    @Query("SELECT DISTINCT o FROM Order o " +
           "LEFT JOIN FETCH o.items " +
           "WHERE o.customerId = :customerId")
    List<Order> findOrdersWithItems(@Param("customerId") Long customerId);
}

3. Index Optimization

@Entity
@Table(indexes = {
    @Index(name = "idx_customer_email", columnList = "email"),
    @Index(name = "idx_order_date", columnList = "orderDate"),
    @Index(name = "idx_status_date", columnList = "status, orderDate")
})
public class Order {
    // ...
}

4. Query Optimization

Before (N+1 Problem):

// This generates N+1 queries!
List<Order> orders = orderRepository.findAll();
orders.forEach(order -> {
    System.out.println(order.getItems().size()); // Triggers query for each order
});

After (Single Query):

@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items")
List<Order> findAllWithItems();

Use Native Queries for Complex Operations:

@Query(value = """
    SELECT * FROM orders o
    WHERE o.status = :status
    AND o.order_date >= :startDate
    AND EXISTS (
        SELECT 1 FROM order_items oi
        WHERE oi.order_id = o.id
        AND oi.quantity > 0
    )
    LIMIT :limit
    """, nativeQuery = true)
List<Order> findActiveOrdersWithItems(
    @Param("status") String status,
    @Param("startDate") LocalDate startDate,
    @Param("limit") int limit
);

Caching Strategies {#caching}

1. Spring Cache Abstraction

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(
            "products", "users", "categories"
        );
        cacheManager.setCaffeine(
            Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(Duration.ofMinutes(10))
                .recordStats()
        );
        return cacheManager;
    }
}

2. Cacheable Methods

@Service
public class ProductService {
    
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        // Expensive database query
        return productRepository.findById(id)
            .orElseThrow();
    }
    
    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }
    
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
    
    @Caching(evict = {
        @CacheEvict(value = "products", allEntries = true),
        @CacheEvict(value = "categories", allEntries = true)
    })
    public void clearAllCaches() {
        // Method implementation
    }
}

3. Redis Caching for Distributed Systems

spring:
  cache:
    type: redis
  redis:
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD}
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
@Configuration
public class RedisCacheConfig {
    
    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .disableCachingNullValues()
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())
            );
    }
}

Connection Pool Tuning {#connection-pools}

HTTP Client Pool Configuration

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = 
            new HttpComponentsClientHttpRequestFactory();
        
        CloseableHttpClient httpClient = HttpClients.custom()
            .setMaxConnTotal(200)
            .setMaxConnPerRoute(20)
            .setConnectionTimeToLive(30, TimeUnit.SECONDS)
            .evictIdleConnections(60, TimeUnit.SECONDS)
            .build();
        
        factory.setHttpClient(httpClient);
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(30000);
        
        return new RestTemplate(factory);
    }
}

WebClient Configuration (Reactive)

@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient() {
        ConnectionProvider provider = ConnectionProvider.builder("custom")
            .maxConnections(500)
            .maxIdleTime(Duration.ofSeconds(20))
            .maxLifeTime(Duration.ofSeconds(60))
            .pendingAcquireTimeout(Duration.ofSeconds(60))
            .evictInBackground(Duration.ofSeconds(120))
            .build();
        
        HttpClient httpClient = HttpClient.create(provider)
            .responseTimeout(Duration.ofSeconds(30))
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }
}

Async Processing {#async-processing}

1. Enable Async Support

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        executor.initialize();
        return executor;
    }
}

2. Async Methods

@Service
public class NotificationService {
    
    @Async
    public CompletableFuture<Void> sendEmail(String to, String subject) {
        // Expensive email sending operation
        emailClient.send(to, subject);
        return CompletableFuture.completedFuture(null);
    }
    
    @Async
    public CompletableFuture<ReportData> generateReport(Long userId) {
        // Long-running report generation
        ReportData data = reportGenerator.generate(userId);
        return CompletableFuture.completedFuture(data);
    }
}

@Service
public class OrderService {
    
    @Autowired
    private NotificationService notificationService;
    
    public Order placeOrder(OrderRequest request) {
        Order order = createOrder(request);
        
        // Fire and forget
        notificationService.sendEmail(
            request.getCustomerEmail(),
            "Order Confirmation"
        );
        
        return order;
    }
}

Monitoring and Profiling {#monitoring}

1. Spring Boot Actuator

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus,info
  metrics:
    export:
      prometheus:
        enabled: true
    distribution:
      percentiles-histogram:
        http.server.requests: true
      slo:
        http.server.requests: 50ms,100ms,200ms,500ms,1s

2. Custom Metrics

@Component
public class OrderMetrics {
    
    private final Counter orderCounter;
    private final Timer orderProcessingTimer;
    
    public OrderMetrics(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Total orders created")
            .tag("type", "online")
            .register(registry);
        
        this.orderProcessingTimer = Timer.builder("order.processing.time")
            .description("Order processing duration")
            .register(registry);
    }
    
    public Order processOrder(OrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = createOrder(request);
            orderCounter.increment();
            return order;
        });
    }
}

3. Profiling with Java Flight Recorder

# Enable JFR
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     -jar application.jar

# Analyze with Mission Control or jfr tool
jfr print --events jdk.CPULoad,jdk.GarbageCollection recording.jfr

Real-World Optimizations {#real-world}

Case Study: Reducing API Response Time from 2s to 200ms

Before:

@GetMapping("/orders/{customerId}")
public List<OrderDTO> getCustomerOrders(@PathVariable Long customerId) {
    List<Order> orders = orderRepository.findByCustomerId(customerId);
    return orders.stream()
        .map(this::toDTO)
        .collect(Collectors.toList());
}

After:

@GetMapping("/orders/{customerId}")
@Cacheable(value = "customerOrders", key = "#customerId")
public List<OrderDTO> getCustomerOrders(@PathVariable Long customerId) {
    // Added JOIN FETCH in repository
    List<Order> orders = orderRepository
        .findByCustomerIdWithItems(customerId);
    
    // Parallel stream for DTO conversion
    return orders.parallelStream()
        .map(this::toDTO)
        .collect(Collectors.toList());
}

Optimizations Applied:

  1. ✅ Added caching layer
  2. ✅ Fixed N+1 query problem with JOIN FETCH
  3. ✅ Used parallel stream for DTO conversion
  4. ✅ Added database index on customer_id

Result: Response time reduced from 2000ms to 180ms (91% improvement)

Performance Checklist

Development:

  • Use appropriate fetch strategies
  • Avoid N+1 queries
  • Implement caching for frequently accessed data
  • Use batch operations for bulk inserts/updates
  • Implement async processing for long-running tasks

Configuration:

  • Tune JVM parameters
  • Configure connection pools properly
  • Enable second-level cache
  • Set up proper indexes
  • Configure GC appropriately

Production:

  • Enable monitoring and metrics
  • Set up alerts for performance degradation
  • Implement distributed tracing
  • Use APM tools (New Relic, Datadog, etc.)
  • Regular performance profiling

Conclusion

Spring Boot performance optimization is an iterative process. Focus on:

  1. Measure first - Use metrics to identify bottlenecks
  2. Optimize systematically - Don’t guess, profile
  3. Test thoroughly - Verify improvements with load tests
  4. Monitor continuously - Performance degrades over time

Remember: Premature optimization is the root of all evil. Optimize based on real metrics and actual bottlenecks.

Resources


What performance optimizations have worked best for your Spring Boot applications? Share your experiences!

Continue reading

Next article

A Comprehensive Enterprise AI Benchmarking Framework for Evaluating Rule-Based, LLM, and Hybrid Agentic Systems

Related Content