September 5, 20247 min

Spring Boot Performance: Beyond the Defaults

Default configurations are starting points, not destinations. How we cut our API response times by 60%.

JavaSpring BootPerformance

Spring Boot's "convention over configuration" philosophy is brilliant for getting started. But in production, those conventions can cost you.

Here's how we systematically improved our payroll API from ~800ms p95 to ~320ms p95.

1. Connection Pool Tuning

The default HikariCP settings are conservative. For our workload:

yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      idle-timeout: 600000

We also added connection pool metrics to Datadog. Watching pool utilization over time revealed that our original pool size of 10 was causing thread contention during payroll calculation windows.

2. JPA N+1 Query Elimination

The silent killer. We used Spring Data JPA's @EntityGraph annotations and switched critical queries to projections:

java
@EntityGraph(attributePaths = {"employee", "deductions"})
List<PayrollRecord> findByPeriod(String period);

This alone cut our main calculation endpoint from 1.2s to 400ms.

3. Redis Caching Strategy

Not everything needs to hit the database. We implemented a tiered caching strategy:

L1: In-process Caffeine cache (5 min TTL) for reference data
L2: Redis cluster for computed results (configurable TTL)
Invalidation: Event-driven via RabbitMQ when source data changes

4. Virtual Threads (Java 21)

Migrating to virtual threads was the final piece. Our I/O-heavy workload benefited massively:

Thread pool management simplified
Throughput increased ~3x under load
Memory footprint decreased significantly

The key takeaway: measure first, optimize second. Every change above was driven by profiling data, not intuition.

Back to Captain's Log