Boost Spring Boot Performance: Proven Tips & Code Samples

This article provides a comprehensive guide to optimizing Spring Boot applications, covering application‑level tweaks, JVM tuning, database connection and query improvements, caching strategies, asynchronous and reactive programming, as well as monitoring and diagnostics, all illustrated with practical code examples.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Boost Spring Boot Performance: Proven Tips & Code Samples

Application Layer Optimization (Spring Boot & Spring Framework)

1. Proper Use of Spring Core Features

@Transactional Transaction Optimization

Limit transaction scope to the smallest necessary unit and avoid opening transactions for read‑only operations.

Specify readOnly = true for read‑only transactions.

Avoid time‑consuming operations (e.g., remote calls) inside transactional methods.

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    // constructor injection
    @Transactional(readOnly = true)
    public List<Order> findAllOrders() {
        return orderRepository.findAll();
    }
    @Transactional
    public Order createOrder(Order order) {
        Order savedOrder = orderRepository.save(order);
        return savedOrder;
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public void updateOrderStatus(Long orderId, OrderStatus status) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
        order.setStatus(status);
        orderRepository.save(order);
    }
}

Lazy Loading and Bean Scope Optimization

Use @Lazy for large beans that are not needed at startup.

Choose appropriate bean scopes (singleton, prototype, request, session) based on usage.

@Configuration
public class AppConfig {
    // Lazy bean example
    @Bean
    @Lazy
    public HeavyComponent heavyComponent() {
        return new HeavyComponent(); // expensive to initialize
    }
    // Prototype scoped bean
    @Bean
    @Scope("prototype")
    public RequestScopedComponent requestScopedComponent() {
        return new RequestScopedComponent();
    }
}

AOP Aspect Optimization

Define precise pointcut expressions to avoid unnecessary interceptions and evaluate the performance impact of AOP.

@Aspect
@Component
public class OptimizedAspect {
    @Pointcut("execution(* com.yourcompany.service.*Service.*(..)) && !execution(* com.yourcompany.service.*Service.get*(..))")
    public void serviceMethodPointcut() {}
    @Around("serviceMethodPointcut()")
    public Object optimizeServiceCalls(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            if (duration > 500) {
                log.warn("Slow method: {} took {}ms", joinPoint.getSignature(), duration);
            }
        }
    }
}

2. Component Scanning

Use @SpringBootApplication(scanBasePackages = "com.yourcompany") or @ComponentScan to limit the scanned packages and reduce startup time and memory usage.

@SpringBootApplication(scanBasePackages = {"com.yourcompany.service", "com.yourcompany.controller.api"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3. Startup Optimization Configuration

Enable global lazy initialization (Spring Boot 2.2+), clean classpath, and use the spring-boot-maven-plugin for executable JAR packaging.

# application.yml
spring:
  main:
    lazy-initialization: true
  web-application-type: servlet
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration
      - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

JVM Layer Optimization

1. JVM Parameter Examples

Typical settings for a 4‑core, 8 GB server:

java -jar app.jar \
  -Xms6g -Xmx6g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:InitiatingHeapOccupancyPercent=75 \
  -XX:+UseStringDeduplication \
  -XX:+UseContainerSupport \
  -XX:MaxRAMPercentage=75.0 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app/heapdump.hprof

For small‑memory microservices:

java -jar app.jar \
  -Xms2g -Xmx2g \
  -XX:+UseSerialGC \
  -XX:NewRatio=3 \
  -XX:SurvivorRatio=4 \
  -XX:MaxTenuringThreshold=15 \
  -XX:+UseCompressedOops \
  -XX:+UseCompressedClassPointers

2. Virtual Thread Configuration (Java 21+)

Spring Boot 3.2+ supports virtual threads:

server:
  tomcat:
    threads:
      virtual:
        enabled: true

Database Layer Optimization

1. HikariCP Connection Pool Settings

Choose HikariCP (default) for best performance; adjust pool size, idle settings, and timeouts according to workload.

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 15
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      pool-name: MyAppHikariCP
      auto-commit: false
      leak-detection-threshold: 60000

2. JPA/Hibernate Optimizations

spring:
  jpa:
    hibernate:
      ddl-auto: validate
    naming:
      physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        jdbc.batch_size: 30
        order_inserts: true
        order_updates: true
        jdbc.batch_versioned_data: true
        show_sql: false
        format_sql: false
        generate_statistics: false
        cache:
          use_second_level_cache: true
          region.factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
          use_query_cache: true
        open-in-view: false

3. SQL Optimization Examples

Use JOIN FETCH or @Fetch(FetchMode.JOIN) to avoid N+1 queries.

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    @Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createTime DESC")
    Page<Order> findByStatus(@Param("status") OrderStatus status, Pageable pageable);

    @Query("SELECT new com.example.dto.OrderSummaryDTO(o.id, o.orderNo, o.createTime, o.status) FROM Order o WHERE o.customerId = :customerId")
    List<OrderSummaryDTO> findOrderSummariesByCustomerId(@Param("customerId") Long customerId);
}

MyBatis Batch Operations

<!-- OrderMapper.xml -->
<insert id="batchInsertOrders">
    INSERT INTO orders (order_no, customer_id, status, create_time)
    VALUES
    <foreach collection="orders" item="order" separator=",">
        (#{order.orderNo}, #{order.customerId}, #{order.status}, #{order.createTime})
    </foreach>
</insert>

Cache Optimization

Local caches (Caffeine, Ehcache) for fast access to small, rarely changing data.

Distributed caches (Redis, Memcached) for shared, large‑scale data with high availability.

1. Redis Cache Integration

@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();
        Map<String, RedisCacheConfiguration> configs = new HashMap<>();
        configs.put("products", defaultConfig.entryTtl(Duration.ofHours(24)));
        configs.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
        configs.put("hotData", defaultConfig.entryTtl(Duration.ofMinutes(10)));
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(configs)
            .transactionAware()
            .build();
    }
}

2. Cache Usage Example

@Service
public class ProductService {
    private final ProductRepository productRepository;
    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public Product findById(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
    }
    @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);
    }
    @CacheEvict(value = "products", allEntries = true)
    @Scheduled(fixedRate = 86400000)
    public void clearProductCache() {
        log.info("Clearing product cache");
    }
}

Cache Protection Techniques

Cache‑penetration guard with null caching and Bloom filter, cache‑avalanche mitigation using random expiration.

@Service
public class ProductServiceImpl implements ProductService {
    private final ProductRepository productRepository;
    private final BloomFilter<Long> productIdBloomFilter;
    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public Product findById(Long id) {
        if (!productIdBloomFilter.mightContain(id)) {
            return null;
        }
        Product product = productRepository.findById(id).orElse(null);
        if (product == null) {
            redisTemplate.opsForValue().set("products::" + id, null, 5, TimeUnit.MINUTES);
        }
        return product;
    }
}

Asynchronous & Concurrency Optimization

1. @Async Methods and Thread‑Pool Configuration

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int core = Runtime.getRuntime().availableProcessors() + 1;
        executor.setCorePoolSize(core);
        executor.setMaxPoolSize(core * 2);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setAwaitTerminationSeconds(60);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }
}

@Service
public class NotificationService {
    private final EmailClient emailClient;
    @Async("taskExecutor")
    public CompletableFuture<Void> sendOrderConfirmationEmail(Order order) {
        try {
            Thread.sleep(1000);
            emailClient.send(order.getCustomerEmail(), "Order Confirmation", "Thank you for your order #" + order.getId());
            return CompletableFuture.completedFuture(null);
        } catch (Exception e) {
            log.error("Failed to send email", e);
            return CompletableFuture.failedFuture(e);
        }
    }
}

2. Reactive Programming (WebFlux)

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;
    @GetMapping("/{id}")
    public Mono<ResponseEntity<OrderDTO>> getOrder(@PathVariable Long id) {
        return orderService.findById(id)
            .map(order -> ResponseEntity.ok(convertToDTO(order)))
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }
    @GetMapping
    public Flux<OrderSummaryDTO> getOrdersByCustomer(@RequestParam Long customerId) {
        return orderService.findByCustomerId(customerId)
            .map(this::convertToSummaryDTO)
            .sort((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime()));
    }
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    public Mono<Order> findById(Long id) {
        return Mono.fromCallable(() -> orderRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found")));
    }
    public Flux<Order> findByCustomerId(Long customerId) {
        return Flux.fromIterable(orderRepository.findByCustomerId(customerId));
    }
}

Monitoring & Diagnostics

Enable Spring Boot Actuator for endpoints such as /actuator/metrics, /actuator/health, and /actuator/prometheus.

Integrate Prometheus + Grafana, ELK stack, or APM tools (New Relic, Dynatrace, SkyWalking, etc.) for metrics, logs, and distributed tracing.

Use profilers like VisualVM, YourKit, JProfiler, or Arthas for CPU, memory, and thread analysis.

Conclusion

Spring Boot performance tuning is a systematic effort that spans multiple layers:

Application layer – leverage Spring features, optimize beans, AOP, and transactions.

JVM layer – choose appropriate GC and tune parameters.

Database layer – fine‑tune connection pools, SQL, and ORM settings.

Cache layer – implement multi‑level caching and protect against penetration, avalanche, and stampede.

Concurrency – use async methods, reactive programming, and proper thread‑pool sizing.

Monitoring – establish comprehensive observability and continuously iterate based on data.

Key recommendations: drive optimizations with data, address the biggest bottlenecks first, avoid premature or excessive tuning, maintain performance test baselines, continuously monitor production metrics, and stay updated with Spring’s latest performance improvements.

CachingSpring Bootjvm-tuningAsync
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.