Why Is Your Spring Boot App Lagging? 10 Optimization Tips to Speed It Up

This guide walks through ten practical techniques—startup lazy initialization, scoped component scanning, selective auto‑configuration, async processing, connection‑pool tuning, JPA batch settings, multi‑level caching, multi‑stage Docker builds, JVM container‑aware flags, Tomcat thread tuning, Resilience4j, observability stack, and TDD—to diagnose and eliminate performance bottlenecks in Spring Boot applications.

Senior Xiao Ying
Senior Xiao Ying
Senior Xiao Ying
Why Is Your Spring Boot App Lagging? 10 Optimization Tips to Speed It Up

Startup Optimization

Goal: Reduce the time from Spring Boot start to ready, crucial for CI/CD and cloud‑native environments.

Lazy Initialization : Enable globally via spring.main.lazy-initialization: true or annotate specific beans with @Lazy so they are created only on first use.

# application.yml
spring:
  main:
    lazy-initialization: true  # Enable lazy init globally [citation:5]
@Service
@Lazy // Enable for this bean only
public class HeavyService {
    // Initialized on first injection or call
}

Limit Component Scan : Restrict scanning to essential packages.

@SpringBootApplication(scanBasePackages = "com.yourcompany.core")
// Or exclude specific packages
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.thirdparty.*"))
public class Application { }

Exclude Unnecessary Auto‑Configuration : Turn off auto‑configs that are not needed, e.g., when no relational DB is used.

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class, // Exclude if no relational DB
    MongoAutoConfiguration.class
})

Asynchronous & Concurrent Processing

Goal: Separate time‑consuming tasks (email, file handling) from request threads to improve throughput and response speed.

Enable Async Support : Add @EnableAsync on a configuration class.

Use @Async with CompletableFuture for non‑blocking calls.

@Service
public class NotificationService {
    @Async // Executed in thread pool [citation:4]
    public CompletableFuture<String> sendAsync(String message) {
        Thread.sleep(1000); // Simulate delay
        return CompletableFuture.completedFuture("Sent: " + message);
    }
}

Configure Dedicated Thread Pool to avoid the default SimpleAsyncTaskExecutor.

spring:
  task:
    execution:
      pool:
        core-size: 5
        max-size: 20
        queue-capacity: 100

Data Access Layer Deep Optimization

Goal: Reduce DB latency and round‑trips.

Connection‑Pool Tuning : Use HikariCP (default in Spring Boot 2.x) with appropriate pool sizes.

spring:
  datasource:
    hikari:
      maximum-pool-size: 20  # Adjust per DB capacity [citation:1]
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000

JPA/Hibernate Batch Operations : Enable batch size and ordering to boost write efficiency.

spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 20
        order_inserts: true
        order_updates: true

Use @BatchSize to mitigate N+1 queries.

@Entity
public class Author {
    @OneToMany(mappedBy = "author")
    @BatchSize(size = 10) // Load multiple books per author
    private List<Book> books;
}

Intelligent Multi‑Level Caching

Goal: Leverage fast memory to avoid repeated DB or downstream calls.

Local Cache (Caffeine) : Suitable for hot data with infrequent changes.

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(1000));
        return manager;
    }
}

@Service
public class ProductService {
    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        // Executes only on cache miss
        return productRepository.findById(id).orElse(null);
    }
    @CacheEvict(value = "products", key = "#id")
    public void updateProduct(Product product) { /* ... */ }
}

Distributed Cache (Redis) : Use spring-boot-starter-data-redis for shared or large‑scale caching.

Containerization & Image Optimization

Goal: Build small, fast‑starting, reproducible container images for cloud‑native deployment.

Multi‑Stage Build : Reduces final image size by >70%.

# Stage 1: Build
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests

# Stage 2: Run
FROM openjdk:17-jdk-slim  # Lightweight runtime [citation:6]
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar  # Copy only the jar [citation:3]
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

JVM Tuning for Cloud‑Native Environments

Goal: Ensure efficient, stable JVM operation inside containers and avoid OOM.

Container‑Aware Memory Settings (JDK 8u191+ / JDK 10+). Use percentage‑based limits.

java -jar \
  -XX:InitialRAMPercentage=50.0 \
  -XX:MaxRAMPercentage=80.0 \
  -XX:+UseContainerSupport \ # Enabled by default on JDK 8u191+ [citation:5]
  app.jar

Tiered Compilation for Faster Startup (sacrifices peak performance for short‑lived functions).

JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

Adjust Thread Stack Size to fit microservice concurrency needs.

JAVA_TOOL_OPTIONS="-Xss256k"  # Test to avoid StackOverflowError [citation:5]

Web Container & Network Transmission Optimization

Goal: Improve HTTP request handling efficiency.

Tomcat Thread Pool tuning based on load.

server:
  tomcat:
    threads:
      max: 200  # Max worker threads [citation:1]
      min-spare: 20
    accept-count: 100  # Queue length [citation:1]
    connection-timeout: 5000ms

Enable HTTP/2 and Compression for better transfer performance.

server:
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,application/json,application/javascript
  http2:
    enabled: true

Rate Limiting, Circuit Breaking & Degradation

Goal: Protect core functionality when downstream services are unstable or under heavy load.

Resilience4j replaces Netflix Hystrix with a lighter, more powerful library.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

Annotations for Rate Limiting & Circuit Breaking .

@Service
public class ExternalApiService {
    // Allow max 2 calls per second
    @RateLimiter(name = "externalApi")
    // Open circuit when failure rate > 50%
    @CircuitBreaker(name = "externalApi", fallbackMethod = "fallback")
    public String callUnstableApi() {
        // Call external API
    }
    private String fallback(Exception e) {
        return "Fallback response due to: " + e.getMessage();
    }
}

Observability: Monitoring, Metrics & Tracing

Goal: Build a complete monitoring system so performance issues are visible, traceable, and alertable.

Core Stack : Spring Boot Actuator, Micrometer, Prometheus + Grafana.

Key Configuration to expose endpoints and enable Prometheus scraping.

management:
  endpoints:
    web:
      exposure:
        include: health, metrics, prometheus  # Expose necessary endpoints
  metrics:
    export:
      prometheus:
        enabled: true
  tracing:
    sampling:
      probability: 1.0  # Set sampling rate

Test‑Driven Development (TDD) to Safeguard Optimizations

Goal: Ensure each performance tweak or refactor is safe, reliable, and sustainable.

Red → Green → Refactor cycle: write failing test, make it pass, then clean code.

Example Test for a Service Method .

@SpringBootTest
class ProductServiceTest {
    @Test
    void getProductById_shouldReturnProduct_whenExists() {
        // Prepare test data
        // Call method under optimization
        // Assert expected result
    }
    @Test
    void getProductById_shouldThrow_whenNotExists() { /* ... */ }
}

Run tests, apply optimization (e.g., add @Cacheable), run tests again to verify no regression.

Summary & Action Roadmap

Measure First : Establish a performance baseline (e.g., JMeter QPS, P95 latency) using Actuator/Prometheus metrics to locate real bottlenecks.

Inside‑Out Approach : Optimize in order – Application/JVM code → Data layer (cache, SQL) → Architecture layer (async, splitting).

Trade‑offs : Each optimization has cost – lazy init adds first‑request latency, caching introduces consistency concerns, async adds system complexity.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaDockerperformance optimizationObservabilityCachingSpring BootasyncTDD
Senior Xiao Ying
Written by

Senior Xiao Ying

Dedicated to sharing Java backend technical experience and original tutorials, offering career transition advice and resume editing. Recognized as a rising star in CSDN's Java backend community and ranked Top 3 in the 2022 New Star Program for Java backend.

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.