Why configuration recipes matter

Great Spring Boot applications don’t just happen—they’re configured. The fastest way to reach a reliable, high‑performance setup is to reuse small, proven configuration “recipes.” The 10 recipes below are concise, copy‑pastable snippets you can drop into your projects to optimize startup, throughput, resilience, and maintainability. They’re designed for Spring Boot 3.x and Java 17+ and come with practical guidance on when and why to use them.

Before you start:

  • Keep configuration centralized in application.yml.
  • Prefer type‑safe @ConfigurationProperties to scattered @Value injections.
  • Test key performance knobs under realistic load; “best” defaults are workload‑dependent.

1) Profile Groups and Layered Configuration

Profile groups help you compose environments (e.g., dev, staging, prod) without duplicating every property. This recipe also demonstrates using a default profile and separating secrets.

When to use

  • You have multiple environments with shared subsets (e.g., “aws” and “prod”).
  • You want predictable overrides with minimal duplication.

Snippet (application.yml)

spring:
  profiles:
    default: local

  config:
    activate:
      on-profile: local

# Base settings apply to all profiles unless overridden
server:
  port: 8080

logging:
  level:
    root: INFO
    com.example: DEBUG

# Group profiles so activating one implies others
spring:
  profiles:
    group:
      prod: [aws, observability]
      staging: [aws]
      local: [devtools, mock-deps]

---
spring:
  config:
    activate:
      on-profile: aws

# AWS-specific overrides
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

---
spring:
  config:
    activate:
      on-profile: prod

server:
  compression:
    enabled: true
  http2:
    enabled: true

# Keep secrets in an external file or environment variables
# e.g., export SPRING_DATASOURCE_URL=...

Why it works

  • Profile groups let you attach common feature sets to environments.
  • Default profile ensures the app is usable locally without extra flags.

Tips

  • Keep production secrets external (environment vars, Vault, AWS Parameter Store).
  • Use spring.config.import to pull centralized configs (e.g., from config server).

2) Type‑Safe Configuration with Validation

Stop sprinkling @Value everywhere. Bind related properties into a single object with validation so misconfiguration fails fast.

When to use

  • You need clarity and validation for critical settings (timeouts, URLs, sizes).

Snippet (Java)

// build.gradle or pom.xml should include starter-validation
// implementation("org.springframework.boot:spring-boot-starter-validation")

package com.example.config;

import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import java.time.Duration;

@Validated
@ConfigurationProperties("client.api")
public class ApiClientProperties {
  @NotBlank
  private String baseUrl;

  @DurationMin(seconds = 1)
  private Duration connectTimeout = Duration.ofSeconds(3);

  @DurationMin(seconds = 1)
  private Duration readTimeout = Duration.ofSeconds(5);

  @Min(1) @Max(1000)
  private int maxConnections = 100;

  // getters/setters omitted for brevity
}
package com.example.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(ApiClientProperties.class)
public class PropertiesConfig {}
# application.yml
client:
  api:
    base-url: https://api.example.com
    connect-timeout: 2s
    read-timeout: 4s
    max-connections: 200

Why it works

  • Validation ensures invalid configs are caught at startup.
  • One cohesive class makes intent and defaults obvious.

Tips

  • Prefer constructor‑bound properties or records for immutability.
  • Use @DurationMin/@DurationMax and size constraints liberally.

3) HikariCP Connection Pool Tuning

Database connection pools often gate your throughput and latency. Tune HikariCP deliberately.

When to use

  • You see connection starvation, high latencies, or DB CPU spikes.
  • You want predictable behavior under load.

Snippet (application.yml)

spring:
  datasource:
    url: jdbc:postgresql://db:5432/app
    username: app
    password: ${DB_PASSWORD}
    hikari:
      pool-name: app-pool
      maximum-pool-size: 30        # Size based on DB cores/queries per tx
      minimum-idle: 10
      connection-timeout: 250ms    # Fail fast to trigger fallbacks
      idle-timeout: 600000         # 10m
      max-lifetime: 1800000        # 30m < DB idle timeout
      leak-detection-threshold: 2000 # Optional; dev only
      auto-commit: false
spring:
  jpa:
    open-in-view: false  # important for performance and correctness
    hibernate:
      ddl-auto: validate

Why it works

  • Tight connection-timeout surfaces backpressure instead of hanging.
  • max-lifetime < DB’s own idle timeout prevents stale connections.
  • Disabling Open Session in View avoids long‑lived transactions across web layers.

Tips

  • Align pool size with database capacity (not just CPU).
  • Monitor hikaricp.connections.* and JDBC timings via Micrometer.

4) Faster Startup and Leaner Memory

Lazy initialization and selective autoconfiguration can cut cold start times and memory.

When to use

  • Serverless / short‑lived workloads.
  • Large monoliths with rarely used beans.

Snippet (application.yml)

spring:
  main:
    lazy-initialization: true
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration # example
      - org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration

spring:
  jackson:
    serialization:
      write-dates-as-timestamps: false

# Keep transactions short; fail fast on slow web requests
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s

Optional: conditional beans

@Bean
@ConditionalOnProperty(name = "feature.x.enabled", havingValue = "true")
public MyHeavyBean heavyBean() { ... }

Why it works

  • Lazy init defers bean creation until first use.
  • Excluding unused autoconfig trims the graph.
  • Graceful shutdown avoids request failures during redeploys.

Tips

  • Don’t overuse lazy init in latency‑sensitive code paths.
  • Profile startup with --debug and ApplicationStartup (Buffering or FlightRecorder).

5) Production‑Ready WebClient with Timeouts, Pooling, and Retries

A hardened HTTP client reduces tail latency and protects upstreams.

When to use

  • Your service calls other services or third‑party APIs.
  • You need timeouts, connection pooling, and standard retry policies.

Snippet (Java)

package com.example.http;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.util.retry.Retry;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

  @Bean
  WebClient webClient(WebClient.Builder builder) {
    ConnectionProvider provider =
        ConnectionProvider.builder("app-webclient")
            .maxConnections(200)
            .pendingAcquireMaxCount(1000)
            .pendingAcquireTimeout(Duration.ofSeconds(1))
            .maxIdleTime(Duration.ofSeconds(30))
            .build();

    HttpClient httpClient = HttpClient.create(provider)
        .compress(true)
        .responseTimeout(Duration.ofSeconds(3))
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
        .doOnConnected(conn -> conn
            .addHandlerLast(new ReadTimeoutHandler(3, TimeUnit.SECONDS))
            .addHandlerLast(new WriteTimeoutHandler(3, TimeUnit.SECONDS)));

    return builder
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .filter(ExchangeFilterFunctions.statusError(
            status -> status.is5xxServerError() || status.value() == 429,
            (req, res) -> new RuntimeException("Upstream error: " + res.statusCode())))
        .filter((req, next) ->
            next.exchange(req)
                .retryWhen(Retry.backoff(3, Duration.ofMillis(200))
                    .filter(ex -> !(ex instanceof IllegalArgumentException))
                    .maxBackoff(Duration.ofSeconds(2))))
        .build();
  }
}

Why it works

  • Connection pool prevents excessive socket churn.
  • Aggressive timeouts and bounded retries protect your service and upstreams.
  • Gzip compression reduces payload sizes.

Tips

  • Externalize numbers with @ConfigurationProperties (see Recipe #2).
  • For synchronous clients, configure RestTemplateBuilder similarly.

6) Blazing‑Fast Cache with Caffeine

A low‑latency local cache lowers DB/HTTP loads and improves tail latency.

When to use

  • Hot reads with moderate cardinality and acceptable staleness.
  • You want per‑cache TTLs and sizes.

Snippet (dependencies)

  • Add spring-boot-starter-cache
  • Add com.github.ben-manes.caffeine:caffeine

Snippet (Java)

package com.example.cache;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.List;

@Configuration
@EnableCaching
public class CacheConfig {

  @Bean
  CacheManager cacheManager() {
    Caffeine<Object, Object> base = Caffeine.newBuilder().recordStats();

    CaffeineCache usersById = new CaffeineCache(
        "usersById",
        base.maximumSize(50_000).expireAfterWrite(Duration.ofMinutes(10)).build());

    CaffeineCache productsBySku = new CaffeineCache(
        "productsBySku",
        base.maximumSize(100_000).expireAfterAccess(Duration.ofMinutes(5)).build());

    SimpleCacheManager mgr = new SimpleCacheManager();
    mgr.setCaches(List.of(usersById, productsBySku));
    return mgr;
  }
}

Usage

@Cacheable(cacheNames = "usersById", key = "#userId")
public User findUser(long userId) { ... }

@CacheEvict(cacheNames = "usersById", key = "#user.id")
public void updateUser(User user) { ... }

Why it works

  • Caffeine offers high hit rates and near‑O(1) operations.
  • Per‑cache settings let you tailor TTLs and sizes.

Tips

  • Monitor cache.gets/cache.hit via Micrometer (Caffeine exposes stats).
  • For distributed caching, add Redis or a second‑level cache alongside Caffeine.

7) Async & Scheduler Thread Pools (with Virtual Threads Option)

Right‑sized executors prevent queue explosions and give predictable latency. Virtual threads can simplify tuning for IO‑heavy workloads.

When to use

  • You use @Async or @Scheduled.
  • You want MDC propagation and bounded queues.

Snippet (Java)

package com.example.async;

import org.slf4j.MDC;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.util.Map;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

  @Bean
  public TaskDecorator mdcTaskDecorator() {
    return runnable -> {
      Map<String, String> contextMap = MDC.getCopyOfContextMap();
      return () -> {
        if (contextMap != null) MDC.setContextMap(contextMap);
        try { runnable.run(); } finally { MDC.clear(); }
      };
    };
  }

  @Bean(name = "appExecutor")
  public Executor appExecutor(TaskDecorator mdcTaskDecorator) {
    ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
    exec.setCorePoolSize(16);
    exec.setMaxPoolSize(64);
    exec.setQueueCapacity(1000);
    exec.setThreadNamePrefix("async-");
    exec.setTaskDecorator(mdcTaskDecorator);
    exec.initialize();
    return exec;
  }

  @Bean
  public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(8);
    scheduler.setThreadNamePrefix("sched-");
    return scheduler;
  }

  // Optional: handle uncaught exceptions from @Async void methods
  @Bean
  public AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler() {
    return (ex, method, params) -> {
      // log or route to observability
    };
  }
}

Optional: flip a switch for virtual threads (Java 21+, Boot 3.2+)

spring:
  threads:
    virtual:
      enabled: true

Or explicitly provide a virtual-thread executor:

@Bean(name = "appExecutor")
public Executor virtualThreadExecutor() {
  return java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor();
}

Why it works

  • Dedicated pools for async and scheduling avoid contention with server worker threads.
  • MDC propagation preserves correlation IDs in logs.
  • Virtual threads drastically simplify IO‑heavy scaling.

Tips

  • Avoid unbounded queues; monitor queue depth and rejection counts.
  • Tune sizes based on CPU (CPU‑bound) or concurrency (IO‑bound).

8) Actuator, Health, Readiness/Liveness, and Endpoint Security

Expose only what you need, secure it, and provide reliable probes for orchestrators.

When to use

  • Kubernetes, ECS, or any orchestrator needs liveness/readiness.
  • You want production observability without oversharing.

Snippet (application.yml)

management:
  server:
    port: 8081
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
      base-path: /actuator
  endpoint:
    health:
      probes:
        enabled: true
      show-details: when_authorized
  metrics:
    tags:
      application: my-app

Spring Security snippet to secure Actuator

package com.example.security;

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class ActuatorSecurityConfig {

  @Bean
  SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
    http
      .securityMatcher(EndpointRequest.toAnyEndpoint())
      .authorizeHttpRequests(auth -> auth
          .requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
          .anyRequest().hasRole("ACTUATOR"))
      .httpBasic(Customizer.withDefaults())
      .csrf(csrf -> csrf.disable());
    return http.build();
  }
}

Custom HealthIndicator

package com.example.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class DownstreamHealthIndicator implements HealthIndicator {
  @Override
  public Health health() {
    // ping a dependency with tight timeout or consult cached results
    boolean ok = true; // replace with real check
    return ok ? Health.up().build() : Health.down().withDetail("service", "down").build();
  }
}

Why it works

  • Separate management port and restricted exposure reduce attack surface.
  • Liveness/readiness integrate with Kubernetes for safe rollouts.
  • Health indicators surface downstream issues quickly.

Tips

  • Add Prometheus metrics via micrometer-registry-prometheus and scrape /actuator/prometheus.
  • Don’t expose /env or /beans publicly.

9) Graceful Shutdown and Server Thread Tuning (Tomcat)

Tune server threads and timeouts to maintain throughput without overwhelming the app during spikes.

When to use

  • High‑traffic APIs that need stable latency.
  • Rolling restarts or blue/green deployments.

Snippet (application.yml)

server:
  shutdown: graceful
  tomcat:
    threads:
      max: 200       # worker threads
      min-spare: 20
    accept-count: 1000  # queued connections when all workers busy
    max-connections: 8192
    connection-timeout: 3s
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,application/json,application/xml
    min-response-size: 1KB
  http2:
    enabled: true

spring:
  lifecycle:
    timeout-per-shutdown-phase: 25s

Why it works

  • Graceful shutdown lets in‑flight requests complete before termination.
  • Right‑sized worker threads and accept queue avoid connection resets.
  • HTTP/2 and compression improve network efficiency.

Tips

  • Match thread counts to CPU and downstream capacity.
  • For WebFlux/Netty, tune Reactor Netty via reactor.netty.http.server.* or dedicated HttpServer.

10) Safe, Automated Database Migrations with Flyway

Database drift causes outages. Make schema changes repeatable and visible.

When to use

  • You ship changes frequently.
  • You want zero‑touch migrations during deploys.

Snippet (application.yml)

spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true       # helpful when adopting Flyway on existing DBs
    out-of-order: false
    clean-disabled: true
    placeholders:
      app_user: app
    placeholder-prefix: ${ # default, explicit for clarity
    placeholder-suffix: }

Example migration

src/main/resources/db/migration/V1__create_tables.sql
CREATE TABLE IF NOT EXISTS users (
  id BIGSERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Why it works

  • Migrations run at startup in a controlled, versioned manner.
  • Baseline avoids clobbering existing schemas when onboarding.

Tips

  • For large/locking migrations, run Flyway via a job at deploy time, not on app startup.
  • Do not enable clean in production.

Bonus mini‑tweaks to consider

  • Disable excessive logs in hot paths:
    logging:
      level:
        org.springframework.jdbc.core.JdbcTemplate: WARN
        org.hibernate.SQL: WARN
        org.hibernate.type.descriptor.sql.BasicBinder: WARN
    
  • Set response compression and cache headers at the framework level for static assets.
  • For JSON, prefer afterburner and sane defaults:
    implementation("com.fasterxml.jackson.module:jackson-module-afterburner")
    
    @Bean
    com.fasterxml.jackson.databind.Module afterburner() {
      return new com.fasterxml.jackson.module.afterburner.AfterburnerModule();
    }
    

Putting it together: a reference application.yml

Use this as a starting point and adjust to your workload.

spring:
  profiles:
    default: local
    group:
      prod: [aws, observability]

  main:
    lazy-initialization: true

  threads:
    virtual:
      enabled: true  # Optional if you use Java 21+

  datasource:
    url: jdbc:postgresql://db:5432/app
    username: app
    password: ${DB_PASSWORD}
    hikari:
      pool-name: app-pool
      maximum-pool-size: 30
      minimum-idle: 10
      connection-timeout: 250ms
      max-lifetime: 1800000
      idle-timeout: 600000
      auto-commit: false

  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate

  flyway:
    enabled: true
    baseline-on-migrate: true
    clean-disabled: true

management:
  server:
    port: 8081
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      probes:
        enabled: true
      show-details: when_authorized

server:
  shutdown: graceful
  http2:
    enabled: true
  compression:
    enabled: true
    min-response-size: 1KB
  tomcat:
    threads:
      max: 200
      min-spare: 20
    accept-count: 1000
    max-connections: 8192
    connection-timeout: 3s

logging:
  level:
    root: INFO
    com.example: DEBUG

Measuring impact: don’t guess—measure

Configuration is only as good as the results it delivers. To verify improvements:

  • Measure startup: compare ApplicationStartedEvent timestamps and memory profiles.
  • Load test: use realistic traffic patterns; watch p95/p99 latency, error rates.
  • Observe: instrument with Actuator and Micrometer (Prometheus/Grafana).
  • Tighten: iterate on pool sizes, timeouts, queues—small changes can be significant.

Common pitfalls to avoid

  • Over‑allocating thread pools: more threads ≠ more throughput; can cause thrashing.
  • Infinite retries: always bound retries and use backoff; fail fast when needed.
  • Huge caches without eviction: memory spikes and GC pauses will hurt tail latency.
  • Exposing actuator endpoints publicly: restrict exposure and require auth.
  • Long transactions and Open Session in View: disable open-in-view and keep transactions short.

Final checklist

  • Profiles and groups keep environment configs maintainable.
  • Properties are type‑safe and validated.
  • Connection pools and executors are tuned and monitored.
  • WebClient has timeouts, pooling, and bounded retries.
  • Caching is explicit with per‑cache TTLs and sizes.
  • Actuator endpoints are secured; health probes are enabled.
  • Server shutdown is graceful; threads are right‑sized.
  • Migrations are automated and safe.

Adopt these recipes incrementally. Each one is a small, purposeful change with a measurable payoff. Combine them to unlock the full potential of your Spring Boot applications—faster startup, lower latency, fewer incidents, and a configuration you can trust.

Share this code profile
Last updated: Oct 04, 2025

More Programming Codes

Discover other Programming codes in this industry

10 Essential Python API Integration Patterns for 2024

Discover Python API integration strategies with ready-to-use code snippets featu...

Oct 10 Read →
Comparing Python and Java for Sorting Algorithms: Which Lang...

Dive into the efficiency and performance of Python and Java sorting algorithms w...

Oct 07 Read →
How to Implement Efficient Search Algorithms in C#: A Step-b...

Master C# search algorithms with this in-depth guide featuring real-world code e...

Oct 06 Read →
Streamlining PHP File Handling: Comprehensive Solutions for...

Master PHP file handling with practical solutions and easy-to-understand code sn...

Oct 03 Read →