Skip to content

Spring Boot Performance Optimization and Scalability Guide

Why a Spring Boot Application Starts Slowly

Section titled “Why a Spring Boot Application Starts Slowly”
  1. Large number of auto-configurations

    • Spring Boot scans many auto-configuration classes using spring.factories / AutoConfiguration.imports.
  2. Component scanning

    • @ComponentScan scans packages for Spring-managed components.
  3. Classpath scanning

    • Libraries such as Hibernate, Jackson, and Bean Validation scan classes.
  4. Bean initialization

    • Thousands of beans may be created during startup.
  5. Database initialization

    • Hibernate schema validation or DDL generation increases startup time.
  6. Heavy logging configuration

  7. Large dependency tree

@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})

Or exclude it via properties instead of code:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring.main.lazy-initialization=true
@ComponentScan(basePackages = "com.myapp.service")

Ahead-of-time compilation significantly reduces startup time.

DevTools provides automatic restart, live reload, and improved logging during development — useful locally, but it adds overhead and should be excluded from production builds (it’s automatically disabled when running from a packaged JAR, but avoid relying on that alone if using an unusual deployment setup).

spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.main.application-startup=buffering

Records timing for each startup step so slow phases can be identified directly, rather than guessed at.

flowchart TD
    A[Application Starts] --> B[Classpath Scan]
    B --> C[Auto Configuration]
    C --> D[Component Scan]
    D --> E[Bean Creation]
    E --> F[Hibernate Initialization]
    F --> G[Application Ready]

Startup delay mainly comes from classpath scanning, bean initialization, Hibernate initialization, and auto-configuration. Optimize using lazy initialization, limiting component scanning, disabling unused auto-configurations, and using Spring Boot AOT/native compilation.


  • Tune JVM heap
Terminal window
-Xms512m
-Xmx1024m
  • Use lazy beans
@Bean
@Lazy
  • Disable Open Session in View
spring.jpa.open-in-view=false
  • Stream large datasets
Stream<User> findAllBy();
  • Optimize caching
@Cacheable("users")

Use bounded caches such as Caffeine or Redis with eviction.

  • Reduce unnecessary object creation
  • Tune database connection pools

Reduce bean initialization, limit cache sizes, stream large datasets, optimize connection pools, and tune JVM heap settings.


CREATE INDEX idx_user_email ON users(email);
Page<User> findAll(Pageable pageable);

The problem: one query loads parent records, then one additional query loads child records for each parent.

1 query -> Employees
N queries -> Orders for each employee

Using @EntityGraph

@EntityGraph

Using JOIN FETCH

@Query("SELECT o FROM Order o JOIN FETCH o.items")

Using batch fetching (loads related entities in batches instead of one-by-one — distinct from the INSERT/UPDATE batching below):

hibernate.default_batch_fetch_size=10
spring.jpa.properties.hibernate.jdbc.batch_size=50
interface UserView {
String getName();
}

Example:

Hibernate + Redis

Occurs when a lazy-loaded association is accessed after the Hibernate session has closed.

User user = userRepository.findById(id);
user.getOrders().size(); // LazyInitializationException
@Query("SELECT u FROM User u JOIN FETCH u.orders")
SELECT new UserDTO(u.name,o.name)
@Transactional
public User getUser() {}
@EntityGraph(attributePaths = "orders")
spring.jpa.open-in-view=true

Spring Boot uses HikariCP by default.

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000

Without pooling, every request opens and closes a database connection.

With pooling, existing connections are reused.

flowchart TD
    A[Application] --> B[Connection Pool]
    B --> C[(Database)]

  • @Async
  • Spring WebFlux (non-blocking I/O)
  • Redis/Caffeine caching
  • ThreadPoolTaskExecutor
  • Rate limiting (Resilience4j, Bucket4j)
  • CDN for static content
  • Kafka/RabbitMQ for asynchronous processing
  • HTTP/2 (multiplexed requests over a single connection) where the client and server both support it
@Async

  • Horizontal scaling
  • Stateless services
  • Redis session store
  • Redis cluster caching
  • Read replicas
  • Database sharding
  • API Gateway
  • Kafka/RabbitMQ
flowchart TD
    LB[Load Balancer]
    LB --> I1[Instance 1]
    LB --> I2[Instance 2]
    LB --> I3[Instance 3]
    I1 --> DB[(Database)]
    I2 --> DB
    I3 --> DB

In practice, an API Gateway typically sits in front of the load balancer:

flowchart TD
    C[Client] --> G[API Gateway]
    G --> LB[Load Balancer]
    LB --> M[Microservices]

Add the Actuator dependency (spring-boot-starter-actuator) and check /actuator/metrics for:

executor.active
executor.completed
executor.queue.size

For deeper Micrometer/Prometheus/Grafana setup, see the Spring Boot production monitoring guide.

  • jstack
  • VisualVM
  • JConsole
  • YourKit

Monitor thread pools using Spring Boot Actuator metrics, Micrometer with Prometheus and Grafana dashboards, JVM tools such as VisualVM, and thread dump analysis using jstack.

  • Startup performance is primarily affected by scanning, auto-configuration, bean creation, and Hibernate initialization.
  • Memory usage is improved through JVM tuning, lazy initialization, streaming, and cache optimization.
  • Database performance depends on indexing, pagination, batching, projections, and efficient fetching.
  • HikariCP is the default connection pool and should be tuned for production workloads.
  • High-throughput systems rely on asynchronous processing, caching, thread-pool tuning, and horizontal scaling.
  • Thread pool health is visible through Actuator executor metrics, complemented by JVM diagnostic tools like jstack and VisualVM.