Skip to main content

Spring Boot โ€” Advanced Topics

Advanced Spring Boot concepts including performance tuning, security practices, reactive programming, distributed systems patterns, and production deployment strategies.


Spring Boot Securityโ€‹

Security Architectureโ€‹

Spring Security in Spring Boot works through a filter chain. Every HTTP request passes through a series of security filters before reaching your controller:

Request โ†’ SecurityFilterChain โ†’ Authentication โ†’ Authorization โ†’ Controller

Default Security Behaviorโ€‹

Adding spring-boot-starter-security immediately:

  • Protects all endpoints with HTTP Basic authentication
  • Generates a random password (printed to console)
  • Enables CSRF protection
  • Creates a default login page at /login

Custom Security Configuration (Spring Boot 3.x)โ€‹

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

JWT Authentication Flowโ€‹

1. Client sends credentials to /auth/login
2. Server validates and returns a JWT
3. Client includes JWT in Authorization header for subsequent requests
4. JwtAuthenticationFilter extracts and validates the token
5. SecurityContext is populated with the authenticated user

Method-Level Securityโ€‹

@Service
public class OrderService {

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public Order getOrder(Long userId, Long orderId) {
// Only admins or the owning user can access
}

@PostAuthorize("returnObject.owner == authentication.name")
public Order findOrder(Long orderId) {
// Filter response โ€” only return if the caller owns it
}
}

Reactive Programming with WebFluxโ€‹

When to Use WebFlux vs MVCโ€‹

AspectSpring MVCSpring WebFlux
ModelThread-per-requestEvent loop (non-blocking)
Best ForTraditional CRUD, blocking I/OHigh concurrency, streaming
ServerTomcat, JettyNetty, Undertow
Data AccessJDBC, JPAR2DBC, reactive MongoDB
BackpressureN/ABuilt-in (Reactive Streams)

Reactive REST Endpointโ€‹

@RestController
@RequestMapping("/api/users")
public class UserController {

private final UserRepository userRepository;

@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll();
}

@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException(id)));
}
}

Key Reactive Typesโ€‹

TypeDescriptionAnalogy
Mono<T>0 or 1 elementOptional<T> or CompletableFuture<T>
Flux<T>0 to N elementsStream<T> or List<T>

Caching Strategiesโ€‹

Spring Cache Abstractionโ€‹

@Service
public class ProductService {

@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
// Called only on cache miss
return productRepository.findById(id).orElseThrow();
}

@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// Always executes, updates cache with return value
return productRepository.save(product);
}

@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
// Removes entry from cache
productRepository.deleteById(id);
}

@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
// Clears the entire cache
}
}

Cache Providersโ€‹

ProviderUse Case
ConcurrentMapCacheDefault, in-memory, single-instance apps
CaffeineHigh-performance in-memory, single-instance
RedisDistributed caching across multiple instances
HazelcastDistributed caching with data grid features
EhCacheFeature-rich, supports disk overflow

Redis Cache Configurationโ€‹

spring:
cache:
type: redis
redis:
time-to-live: 600000 # 10 minutes
data:
redis:
host: localhost
port: 6379

Exception Handling Patternsโ€‹

Global Exception Handlerโ€‹

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();

ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
errors,
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(error);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}

Problem Details (RFC 7807) โ€” Spring Boot 3.xโ€‹

@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage()
);
problem.setTitle("Resource Not Found");
problem.setProperty("timestamp", Instant.now());
return problem;
}

Database Migration with Flyway / Liquibaseโ€‹

Flywayโ€‹

Spring Boot auto-configures Flyway when it's on the classpath. Migrations are SQL files in src/main/resources/db/migration/:

db/migration/
โ”œโ”€โ”€ V1__Create_users_table.sql
โ”œโ”€โ”€ V2__Add_email_column.sql
โ””โ”€โ”€ V3__Create_orders_table.sql
-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Liquibaseโ€‹

Alternative to Flyway using XML/YAML/JSON changelogs:

databaseChangeLog:
- changeSet:
id: 1
author: dev
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true

Performance Tuningโ€‹

JVM and Server Tuningโ€‹

server:
tomcat:
threads:
max: 200 # Max worker threads
min-spare: 10 # Min idle threads
max-connections: 10000
accept-count: 100
connection-timeout: 20000

Connection Pool Tuning (HikariCP)โ€‹

For detailed guidelines on pool sizing, parameter details, and starvation patterns, see the Database Connection Pooling guide. For how HikariCP relates to Tomcat threads, Netty, and production sizing, see Thread Pools, Netty, Tomcat & HikariCP.

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

JPA Performanceโ€‹

spring:
jpa:
open-in-view: false # Disable OSIV โ€” prevents lazy loading in views
properties:
hibernate:
default_batch_fetch_size: 16
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
generate_statistics: true # Enable for debugging, disable in prod

Common Performance Anti-Patternsโ€‹

Anti-PatternImpactSolution
N+1 query problemExcessive DB callsUse JOIN FETCH, @EntityGraph, or batch fetching
Open Session in View (OSIV)DB connection held through view renderingSet spring.jpa.open-in-view=false
No connection pool tuningConnection exhaustion under loadConfigure HikariCP (see Connection Pooling)
Unbounded queriesMemory exhaustionAlways use pagination (Pageable)
Missing indexesSlow queriesAnalyze query plans, add database indexes
Synchronous external callsThread starvationUse async (@Async) or reactive patterns

Graceful Shutdownโ€‹

Spring Boot 2.3+ supports graceful shutdown:

server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s

Behavior:

  1. Stop accepting new requests
  2. Wait for in-flight requests to complete (up to timeout)
  3. Shut down the application context
  4. Destroy beans (calls @PreDestroy)

Observabilityโ€‹

Distributed Tracing with Micrometerโ€‹

Spring Boot 3.x integrates with Micrometer Observation API:

management:
tracing:
sampling:
probability: 1.0 # Sample 100% of requests (reduce in production)
endpoints:
web:
exposure:
include: health, metrics, prometheus
metrics:
distribution:
percentiles-histogram:
http.server.requests: true

Custom Metricsโ€‹

@Service
public class OrderService {

private final Counter orderCounter;
private final Timer orderTimer;

public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.created")
.description("Number of orders created")
.register(registry);
this.orderTimer = Timer.builder("orders.processing.time")
.description("Order processing time")
.register(registry);
}

public Order createOrder(OrderRequest request) {
return orderTimer.record(() -> {
Order order = processOrder(request);
orderCounter.increment();
return order;
});
}
}

Docker & Containerizationโ€‹

Layered JAR for Efficient Docker Buildsโ€‹

Spring Boot 2.3+ produces layered JARs for better Docker caching:

FROM eclipse-temurin:21-jre as builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Cloud Native Buildpacksโ€‹

No Dockerfile needed:

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest

Virtual Threads (Spring Boot 3.2+ / Java 21)โ€‹

Virtual threads (Project Loom) replace the traditional thread-per-request model with lightweight JVM-managed threads, enabling far greater concurrency without reactive code.

Enabling Virtual Threadsโ€‹

spring:
threads:
virtual:
enabled: true # Spring Boot 3.2+ โ€” Tomcat uses virtual threads automatically
// Custom async executor with virtual threads
@Bean
public Executor taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}

Virtual Threads vs Reactive (WebFlux)โ€‹

AspectVirtual ThreadsWebFlux (Reactive)
Programming modelFamiliar blocking codeReactive (Mono/Flux)
Code complexityLowHigh
Blocking I/OSafe โ€” virtual threads park, not block OS threadsMust avoid โ€” blocks the event loop
ThroughputVery high for I/O-bound workloadsVery high
CPU-bound workNo gain over platform threadsNo gain
Migration costMinimal โ€” existing code worksFull rewrite to reactive

Virtual Thread Pitfallsโ€‹

// โŒ Synchronized blocks pin the carrier thread โ€” kills virtual thread benefits
@Service
public class LegacyService {
public synchronized void criticalSection() { // Pins OS thread during blocking ops
jdbcTemplate.query(...); // Blocks inside synchronized = carrier thread blocked
}
}

// โœ… Use ReentrantLock instead for virtual-thread-friendly locking
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock();
try {
jdbcTemplate.query(...); // Virtual thread parks โ€” carrier thread freed
} finally {
lock.unlock();
}
}

@Async โ€” Thread Pool Design and Pitfallsโ€‹

Custom Thread Pool Executorโ€‹

The default @Async executor is SimpleAsyncTaskExecutor โ€” it creates a new thread per invocation with no pooling. Always configure a custom executor in production:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
}
}

@Async Pitfallsโ€‹

1. Exceptions from void async methods are silently swallowed without a handler:

@Async
public void sendEmail(String to) {
throw new RuntimeException("SMTP failed");
// โŒ Exception is LOST unless AsyncUncaughtExceptionHandler is configured
}

2. Return type CompletableFuture is required to propagate exceptions:

@Async
public CompletableFuture<String> fetchData() {
try {
return CompletableFuture.completedFuture(externalApi.fetch());
} catch (Exception e) {
return CompletableFuture.failedFuture(e); // โœ… Caller can handle it
}
}

3. SecurityContext is NOT propagated to @Async threads by default:

// โŒ The async thread has no SecurityContext โ€” authentication.getName() returns null
@Async
public void processUserData() {
String user = SecurityContextHolder.getContext().getAuthentication().getName();
}

// โœ… Fix: configure DelegatingSecurityContextAsyncTaskExecutor
@Bean
public Executor securityAwareAsyncExecutor() {
return new DelegatingSecurityContextAsyncTaskExecutor(taskExecutor());
}

4. Self-invocation bypasses @Async โ€” same proxy problem as @Transactional.


@Retryable โ€” Retry with Spring Retryโ€‹

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
@Configuration
@EnableRetry
public class RetryConfig { }

@Service
public class PaymentService {

@Retryable(
retryFor = {PaymentGatewayException.class, SocketTimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 500, multiplier = 2, maxDelay = 5000)
)
public PaymentResult charge(PaymentRequest request) {
return gatewayClient.charge(request);
}

@Recover
public PaymentResult fallback(PaymentGatewayException ex, PaymentRequest request) {
// Called when all retries are exhausted
log.error("Payment failed after retries: {}", request.getOrderId());
return PaymentResult.failed("GATEWAY_UNAVAILABLE");
}
}

Spring Retry vs Resilience4jโ€‹

FeatureSpring RetryResilience4j
Retryโœ…โœ…
Circuit BreakerโŒโœ…
Rate LimiterโŒโœ…
BulkheadโŒโœ…
Time LimiterโŒโœ…
Reactive supportLimitedNative
Best forSimple retry scenariosProduction resilience patterns

Senior recommendation: Use Resilience4j for production microservices โ€” it covers the full resilience pattern set. Use Spring Retry only if you need quick retry-only behavior.


@Scheduled in Clustered Environmentsโ€‹

@Scheduled runs on every node in a cluster by default. This causes duplicate execution โ€” a dangerous behavior for jobs that modify data, send emails, or trigger payments.

The Problemโ€‹

@Scheduled(cron = "0 0 2 * * *") // Runs at 2 AM
public void generateDailyReport() {
// โŒ Runs on ALL 5 nodes simultaneously โ†’ 5 duplicate reports
}

Fix: ShedLock (Distributed Lock)โ€‹

<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
</dependency>
-- Required schema
CREATE TABLE shedlock (
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP NOT NULL,
locked_at TIMESTAMP NOT NULL,
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class SchedulerConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}

@Scheduled(cron = "0 0 2 * * *")
@SchedulerLock(name = "dailyReport", lockAtLeastFor = "5m", lockAtMostFor = "10m")
public void generateDailyReport() {
// โœ… Only ONE node acquires the lock and runs this โ€” others skip
}

Java Records in Spring Bootโ€‹

Java 14 introduced Records as a concise way to model immutable data carriers. In Spring Boot 3.x (which requires Java 17+), records have become the standard choice for data transfer.

1. REST Controllers & DTOsโ€‹

Records automatically integrate with Jackson for JSON serialization and deserialization. Since they have no setters, they guarantee request and response payloads remain immutable during processing.

public record UserRegistrationRequest(
@NotBlank String username,
@Email String email,
@Size(min = 8) String password
) {}

@RestController
@RequestMapping("/api/users")
public class UserRegistrationController {

@PostMapping
public ResponseEntity<Void> register(@Valid @RequestBody UserRegistrationRequest request) {
// request.username() to access field (no getUsername())
return ResponseEntity.ok().build();
}
}

2. Spring Data JPA Projectionsโ€‹

Instead of fetching full managed entities, database read queries can fetch light immutable record projections directly:

public record UserSummary(Long id, String username, String email) {}

public interface UserRepository extends JpaRepository<User, Long> {

// Class-based DTO projection
List<UserSummary> findByActiveTrue();

// Constructor projection using JPQL
@Query("SELECT new com.example.dto.UserSummary(u.id, u.username, u.email) FROM User u WHERE u.active = true")
List<UserSummary> findActiveUserSummaries();
}

3. Immutable Configuration Propertiesโ€‹

In Spring Boot 3.x, @ConfigurationProperties supports constructor binding on record types out of the box, removing the need for boilerplate getters/setters or Lomboks.

@ConfigurationProperties(prefix = "app.security")
public record SecurityProperties(
String jwtSecret,
Duration tokenValidity,
List<String> allowedOrigins
) {}

To enable this, annotate your configuration with @ConfigurationPropertiesScan or @EnableConfigurationProperties(SecurityProperties.class).


Summaryโ€‹

Advanced Spring Boot development requires understanding:

  • Security โ€” Filter chains, JWT, method-level authorization
  • Reactive โ€” WebFlux for high-concurrency non-blocking apps
  • Caching โ€” Abstraction layer with pluggable providers
  • Performance โ€” Connection pools, JPA tuning, avoiding anti-patterns
  • Observability โ€” Metrics, tracing, and health indicators
  • Deployment โ€” Graceful shutdown, layered Docker images, buildpacks
  • Virtual Threads โ€” Spring Boot 3.2+ Loom integration for blocking I/O at scale
  • Async Safety โ€” Thread pool design, exception handling, SecurityContext propagation
  • Retry Patterns โ€” Spring Retry for simple cases, Resilience4j for production resilience
  • Scheduled Job Safety โ€” ShedLock for cluster-aware scheduling

Advanced Editorial Pass: Advanced Spring Boot Trade-offsโ€‹

Core Engineering Tensionsโ€‹

  • Throughput vs consistency when combining caching, async execution, and transactional boundaries.
  • Fast startup vs comprehensive observability instrumentation.
  • Convention speed vs explicit control for long-lived, critical services.

Common High-Maturity Pitfallsโ€‹

  • Annotation-heavy architecture that hides transactional and retry semantics.
  • Performance tuning done without representative traffic models.
  • Over-centralized base configuration that blocks service-level autonomy.

Review Checklistโ€‹

  1. Validate tuning with load profiles that match real latency distributions.
  2. Make cross-cutting behavior explicit (retries, timeouts, cache invalidation).
  3. Keep advanced defaults documented with rationale and rollback plans.

Compare Nextโ€‹


Interview Questionsโ€‹

Q: How do you choose between MVC with virtual threads and WebFlux?โ€‹

A: Prefer MVC plus virtual threads for simpler code with high I/O concurrency; choose WebFlux for fully non-blocking end-to-end pipelines.

Q: What is the most common production mistake with @Async?โ€‹

A: Using default executor settings, leading to uncontrolled thread growth or weak error handling.

Q: How should retries be designed to avoid cascading failures?โ€‹

A: Combine bounded retries with backoff, jitter, timeout budgets, and circuit breakers.

Q: What does good graceful shutdown protect against?โ€‹

A: Request loss during rolling deployments and incomplete writes during pod termination.

Q: Why is cache strategy an architecture decision, not an annotation decision?โ€‹

A: TTL, invalidation, consistency, and failure behavior must align with business correctness, not just performance.

Q: How do you make observability actionable in advanced Boot services?โ€‹

A: Define SLO-driven metrics, trace critical paths, and correlate logs with trace/span identifiers.

Q: What is a safe way to introduce virtual threads in an existing service?โ€‹

A: Roll out gradually, profile blocking hotspots, and remove synchronized pinning points before broad enablement.