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)

spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1800000
connection-timeout: 30000
leak-detection-threshold: 60000

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 appropriately
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

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

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