Spring Boot Performance Optimization: Expert Tips and Techniques
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
- Performance Metrics That Matter
- JVM and Memory Optimization
- Database Performance
- Caching Strategies
- Connection Pool Tuning
- Async Processing
- Monitoring and Profiling
- 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
G1GC (Recommended for most applications)
-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:
- ✅ Added caching layer
- ✅ Fixed N+1 query problem with JOIN FETCH
- ✅ Used parallel stream for DTO conversion
- ✅ 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:
- Measure first - Use metrics to identify bottlenecks
- Optimize systematically - Don’t guess, profile
- Test thoroughly - Verify improvements with load tests
- Monitor continuously - Performance degrades over time
Remember: Premature optimization is the root of all evil. Optimize based on real metrics and actual bottlenecks.
Resources
- Spring Boot Performance Documentation
- HikariCP Performance Tips
- JVM Performance Optimization
- Hibernate Performance Tuning
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
React Performance Optimization: Complete Guide to Building Fast Applications
Master React performance optimization with proven techniques. Learn code splitting, memoization, lazy loading, Virtual DOM optimization, and advanced patterns to build lightning-fast React applications.
Frontend Performance Optimization: Complete Guide to Building Fast Web Apps
Master frontend performance optimization with proven techniques for loading speed, runtime performance, bundle size, critical rendering path, and Core Web Vitals. Complete guide with practical examples.
Python Best Practices and Advanced Techniques: Complete Professional Guide
Master Python programming with professional best practices, advanced patterns, type hints, performance optimization, testing strategies, and modern Python features. Complete guide for experienced developers.