| name | spring-boot-patterns |
| description | Spring Boot 3.x idioms, auto-configuration, profiles, and production readiness patterns |
Spring Boot 3.x Patterns
Configuration Management
Configuration Properties with Validation
@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
@NotBlank String name,
@Valid Api api,
@Valid Security security,
@Valid Resilience resilience
) {
public record Api(
@NotBlank String basePath,
@Min(1) @Max(100) int defaultPageSize,
@NotNull Duration requestTimeout,
boolean corsEnabled
) {}
public record Security(
@NotBlank String issuerUri,
List<String> allowedOrigins,
Duration tokenExpiration
) {}
public record Resilience(
@Min(1) int maxRetries,
Duration retryDelay,
Duration circuitBreakerTimeout,
int failureThreshold
) {}
}
Profile-Based Configuration
app:
name: my-service
api:
base-path: /api/v1
default-page-size: 20
request-timeout: 30s
cors-enabled: false
spring:
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
jackson:
default-property-inclusion: non_null
deserialization:
fail-on-unknown-properties: true
---
spring:
config:
activate:
on-profile: local
datasource:
url: jdbc:postgresql://localhost:5432/appdb
username: app
password: secret
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
app:
api:
cors-enabled: true
---
spring:
config:
activate:
on-profile: production
datasource:
url: ${DATABASE_URL}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 20000
Auto-Configuration
Custom Starter Auto-Configuration
@AutoConfiguration
@ConditionalOnClass(NotificationService.class)
@EnableConfigurationProperties(NotificationProperties.class)
public class NotificationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "app.notification", name = "enabled", havingValue = "true")
public NotificationService notificationService(NotificationProperties props) {
return new DefaultNotificationService(props);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(NotificationService.class)
public NotificationHealthIndicator notificationHealthIndicator(
NotificationService service) {
return new NotificationHealthIndicator(service);
}
}
Production Readiness
Actuator Configuration
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env
base-path: /management
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
group:
readiness:
include: db,redis,diskSpace
liveness:
include: ping
info:
env:
enabled: true
git:
mode: full
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:default}
Custom Health Indicator
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final ExternalServiceClient client;
@Override
public Health health() {
try {
var response = client.healthCheck();
if (response.isHealthy()) {
return Health.up()
.withDetail("version", response.version())
.withDetail("responseTime", response.latencyMs() + "ms")
.build();
}
return Health.down()
.withDetail("reason", response.message())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Graceful Shutdown
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
@Component
@Slf4j
public class GracefulShutdownHandler implements DisposableBean {
private final TaskExecutor taskExecutor;
@Override
public void destroy() {
log.info("Graceful shutdown initiated -- completing in-flight requests");
if (taskExecutor instanceof ThreadPoolTaskExecutor pool) {
pool.setWaitForTasksToCompleteOnShutdown(true);
pool.setAwaitTerminationSeconds(30);
}
}
}
REST Client Configuration
@HttpExchange("/api/v1/users")
public interface UserServiceClient {
@GetExchange("/{id}")
UserDto getUser(@PathVariable String id);
@GetExchange
List<UserDto> listUsers(@RequestParam("role") String role);
@PostExchange
UserDto createUser(@RequestBody CreateUserRequest request);
}
@Configuration
public class ClientConfig {
@Bean
public UserServiceClient userServiceClient(
RestClient.Builder builder,
AppProperties props) {
var restClient = builder
.baseUrl(props.services().userServiceUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.requestInterceptor(new CorrelationIdInterceptor())
.build();
var factory = HttpServiceProxyFactory.builderFor(
RestClientAdapter.create(restClient)).build();
return factory.createClient(UserServiceClient.class);
}
}
Caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
var caffeine = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats();
return new CaffeineCacheManager("products", "categories");
}
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public ProductDto findById(String id) {
return productRepository.findById(id)
.map(productMapper::toDto)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CacheEvict(value = "products", key = "#id")
@Transactional
public ProductDto update(String id, UpdateProductRequest request) {
}
@CacheEvict(value = "products", allEntries = true)
@Transactional
public void deleteAll() {
productRepository.deleteAll();
}
}
Spring Boot Checklist
- Configuration --
@ConfigurationProperties with validation, profile separation
- Auto-Configuration -- Leverage starters, custom auto-config where needed
- Dependency Injection -- Constructor injection, no field
@Autowired
- Actuator -- Health, metrics, info endpoints enabled and secured
- Graceful Shutdown -- Configured with appropriate timeout
- Caching -- Cache manager configured,
@Cacheable on read-heavy operations
- HTTP Clients -- Declarative
@HttpExchange interfaces with timeouts
- Open-in-View -- Disabled (
spring.jpa.open-in-view=false)
- Jackson -- Configured for strict deserialization, non-null inclusion
- Profiles -- Environment-specific configs, secrets externalized