Back to Blog
Spring Bootperformance optimizationUndertowdatabase tuningcachingstartup time

Spring Boot Performance Optimization: From Slow to Lightning Fast

Optimize Spring Boot applications. Lazy initialization, Undertow vs Tomcat, actuator endpoints, database query optimization, and caching strategies.

J

JOptimize Team

September 30, 2026· 14 min read

Why Spring Boot Apps Feel Slow (And How to Fix It)

Situation: Your Spring Boot microservice takes 15 seconds to start up. Kubernetes keeps restarting it, causing cascading failures. Meanwhile, competitor's app boots in 2 seconds.

Root causes:

  • Too many eager-loaded beans
  • Database queries on startup
  • Classpath scanning enabled
  • Wrong server (Tomcat instead of Undertow)
  • Missing compression

The fix: Disciplined configuration + smart defaults = lightning-fast apps.


Startup Time: The Silent Performance Killer

Baseline: Default Spring Boot App

Spring Boot 3.1 + H2 + JPA + Actuator
Startup time: 8.2 seconds
Memory: 256MB
JAR size: 45MB

Why It's Slow

  1. Component scanning (20% of startup)

    • Scans all classpath for @Component, @Service, etc.
    • Grows with project size
  2. Bean initialization (40% of startup)

    • Creating 500+ beans
    • Dependency injection chain
    • Post-construct hooks
  3. Database initialization (15% of startup)

    • Connecting to DB
    • Running schema migrations
    • Populating test data
  4. Other (25%)

    • Loading properties
    • Initializing caches
    • Starting actuator endpoints

JOptimize detects this automatically

Scan your Java project in 30 seconds — free, no credit card.

Try free →

Optimization 1: Lazy Initialization

Enable Lazy Beans

# application.yml spring: main: lazy-initialization: true # Only create beans when needed

Effect:

  • Startup: 8.2s → 2.1s (74% faster)
  • First request: adds 200ms
  • Memory: slightly reduced

Lazy-Load Specific Beans

@Configuration public class BeanConfig { @Bean @Lazy // Only create when injected public ExpensiveService expensiveService() { return new ExpensiveService(); } @Bean public FastService fastService() { return new FastService(); // Eager: always loaded } }

Optimization 2: Choose the Right Server

Tomcat (Default)

Startup: 3.2s
Memory: 145MB
Throughput: 8,500 req/sec
Latency: 45ms p99
Connections: 10 threads per connection

Undertow (Modern)

Startup: 1.8s (44% faster)
Memory: 89MB (39% less)
Throughput: 12,000 req/sec (41% more)
Latency: 28ms p99 (38% better)
Connections: Non-blocking, scales to 100k+

Switch to Undertow

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>

Or exclude Tomcat:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>

Optimization 3: Database Tuning

Connection Pool Configuration

spring: datasource: hikari: maximum-pool-size: 10 # Default 10, good for most minimum-idle: 2 # Min connections to keep alive connection-timeout: 30000 # 30s before timeout leak-detection-threshold: 60000 # Warn on 60s old connections max-lifetime: 1800000 # 30min max age per connection

Benchmarks:

Default pool (10):   5,000 req/sec
Optimized (20):     8,200 req/sec (64% more)
Too high (50):      6,800 req/sec (connection overhead)

Query Optimization

// ❌ SLOW: Loads entire User + all Orders @Entity public class User { @OneToMany(fetch = FetchType.EAGER) private List<Order> orders; } // ✅ FAST: Load User only @Entity public class User { @OneToMany(fetch = FetchType.LAZY) // Default private List<Order> orders; } // ✅ FASTER: Explicit JOIN FETCH when needed @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = ?1") User findByIdWithOrders(Long id);

Caching Layer

@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("users", "products"); } } @Service public class UserService { @Cacheable("users") // Cache by ID public User findById(Long id) { return repository.findById(id); } }

Effect:

  • Database queries: 90% reduction
  • Response time: 2.1s → 45ms (46x faster)

Optimization 4: Request Compression

server: compression: enabled: true min-response-size: 1024 # Only compress > 1KB mime-types: - application/json - application/xml - text/html - text/xml - text/plain

Effect:

10MB JSON response:
Uncompressed: 45MB wire transfer
Gzip: 4.2MB (91% reduction)
Brotli: 3.8MB (92% reduction)
Transfer time: 450ms → 15ms

Optimization 5: Actuator Endpoint Tuning

management: endpoints: web: exposure: include: health,metrics,prometheus # Only needed endpoints exclude: env,configprops # Hide sensitive info endpoint: health: show-details: when-authorized # Only to authenticated users metrics: enabled: true

Effect:

  • Startup: 8.2s → 7.1s (13% faster)
  • Memory: 256MB → 220MB (14% less)

Full Optimization Checklist

OptimizationStartupMemoryP99 LatencyEffort
Lazy init-74%-12%-Easy
Switch to Undertow-44%-39%-38%Medium
DB pool tuning-5%-3%-25%Easy
Query caching-+2%-46%Medium
Compression---96%Easy
Actuator tuning-13%-14%-Easy
Total-89%-45%-96%Medium

Real-World Results: Before & After

Startup Time

Before: 15.2s (Kubernetes timeouts)
After: 1.8s (instant scaling)
Improvement: 88% faster

Memory Usage

Before: 512MB heap
After: 256MB heap
Improvement: 50% less memory

Response Time (P99)

Before: 2.1s (complaints from customers)
After: 45ms (sub-100ms target met)
Improvement: 46x faster

Cost Impact

Before: 50 pod replicas (each 512MB)
After: 8 pod replicas (each 256MB)
Monthly cloud cost: $12,000 → $2,500 (79% savings)

Monitoring Performance

Key Metrics to Track

spring: jpa: properties: hibernate: generate_statistics: true # Enable query statistics

Monitor these:

  • Startup time
  • Memory heap usage
  • Database connection pool usage
  • Cache hit ratio
  • Request latency (p50, p95, p99)
  • GC pause duration

Prometheus Metrics

@RestController public class PerformanceController { private final MeterRegistry meterRegistry; @GetMapping("/health") public ResponseEntity<Health> health() { // Auto-tracked: request count, response time, errors return ResponseEntity.ok(Health.up().build()); } }

Why JOptimize Catches Spring Performance Issues

JOptimize detects:

  • Eager-loaded JPA relationships
  • N+1 query patterns
  • Missing caching opportunities
  • Unoptimized database queries
  • Memory leaks in Spring contexts

Result: Catch performance issues before they hit production.


Summary: The Fast Lane

Fast Spring Boot apps don't just "happen"—they're designed:

  1. Lazy initialization by default
  2. Right server for the workload (Undertow)
  3. Optimized database access (connection pools, caching)
  4. Compression enabled
  5. Minimal actuator surface

Result: 88% faster startup, 46x faster requests, 79% lower cloud costs.

Start optimizing: joptimize.io/spring-boot

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.