Java continues to evolve at an unprecedented pace, with new features being released every six months. Yet many developers are still stuck using Java 8 patterns and missing out on powerful modern features that can dramatically improve code quality, performance, and developer productivity.
In this comprehensive guide, we'll explore Java 21's revolutionary features and dive deep into Spring Boot best practices that most developers overlook. You'll learn how to leverage Virtual Threads, Pattern Matching, Records, and advanced Spring Boot techniques to build high-performance, maintainable applications.
What You'll Master in This Guide
By the end of this tutorial, you'll have learned:
- 🚀 Virtual Threads for massive concurrency improvements
- 🔍 Pattern Matching and Switch Expressions for cleaner code
- 📊 Records and their advanced use cases beyond simple DTOs
- ⚡ Spring Boot 3.2+ performance optimizations
- 🏗️ Modern architectural patterns with Spring Boot
- 🛡️ Security best practices for production applications
- 📈 Observability and monitoring with Micrometer
- 🐳 Containerization strategies for Java applications
- 🔧 Advanced configuration techniques
- 📱 Reactive programming with Spring WebFlux
Java 21: The Game-Changing LTS Release
Java 21 is the latest Long Term Support (LTS) release, packed with features that fundamentally change how we write Java code. Let's explore the most impactful ones:
Virtual Threads: Revolutionizing Concurrency
Virtual Threads are perhaps the most significant addition to Java in years. They allow you to create millions of lightweight threads without the traditional overhead.
Before Virtual Threads (Traditional Approach):
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// This blocks a platform thread while waiting for I/O
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private ExternalApiClient apiClient;
public User findById(Long id) {
// Each I/O operation blocks a precious platform thread
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
// External API call blocks thread
UserProfile profile = apiClient.getUserProfile(user.getEmail());
user.setProfile(profile);
return user;
}
}
With Virtual Threads (Modern Approach):
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// Enable Virtual Threads globally
System.setProperty("spring.threads.virtual.enabled", "true");
SpringApplication.run(Application.class, args);
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// Now runs on a virtual thread - can handle millions of concurrent requests
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@GetMapping("/users/batch")
public ResponseEntity<List<User>> getUsers(@RequestParam List<Long> ids) {
// Process thousands of requests concurrently
List<User> users = ids.parallelStream()
.map(userService::findById)
.collect(Collectors.toList());
return ResponseEntity.ok(users);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private ExternalApiClient apiClient;
public User findById(Long id) {
// Each virtual thread can handle blocking I/O efficiently
return userRepository.findById(id)
.map(user -> enrichUserData(user))
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
private User enrichUserData(User user) {
// Multiple I/O operations that would block platform threads
// Now run efficiently on virtual threads
CompletableFuture<UserProfile> profileFuture =
CompletableFuture.supplyAsync(() -> apiClient.getUserProfile(user.getEmail()));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> apiClient.getUserOrders(user.getId()));
// Combine results without blocking threads unnecessarily
try {
UserProfile profile = profileFuture.get(5, TimeUnit.SECONDS);
List<Order> orders = ordersFuture.get(5, TimeUnit.SECONDS);
user.setProfile(profile);
user.setRecentOrders(orders);
return user;
} catch (Exception e) {
log.warn("Failed to enrich user data", e);
return user;
}
}
}
Pattern Matching and Switch Expressions
Pattern matching makes code more readable and less error-prone:
Old Java Approach:
@Service
public class PaymentProcessor {
public PaymentResult processPayment(Payment payment) {
if (payment instanceof CreditCardPayment) {
CreditCardPayment ccPayment = (CreditCardPayment) payment;
return processCreditCard(ccPayment.getCardNumber(), ccPayment.getAmount());
} else if (payment instanceof PayPalPayment) {
PayPalPayment ppPayment = (PayPalPayment) payment;
return processPayPal(ppPayment.getEmail(), ppPayment.getAmount());
} else if (payment instanceof BankTransferPayment) {
BankTransferPayment btPayment = (BankTransferPayment) payment;
return processBankTransfer(btPayment.getAccountNumber(), btPayment.getAmount());
} else {
throw new UnsupportedPaymentMethodException("Unsupported payment method");
}
}
}
Modern Java 21 Approach:
@Service
public class PaymentProcessor {
public PaymentResult processPayment(Payment payment) {
return switch (payment) {
case CreditCardPayment(var cardNumber, var amount, var expiryDate) ->
processCreditCard(cardNumber, amount);
case PayPalPayment(var email, var amount) ->
processPayPal(email, amount);
case BankTransferPayment(var accountNumber, var amount, var routingNumber) ->
processBankTransfer(accountNumber, amount);
case CryptoPayment(var walletAddress, var amount, var currency)
when currency.equals("BTC") ->
processBitcoin(walletAddress, amount);
case CryptoPayment(var walletAddress, var amount, var currency) ->
processCrypto(walletAddress, amount, currency);
default -> throw new UnsupportedPaymentMethodException(
"Unsupported payment method: " + payment.getClass().getSimpleName());
};
}
// Advanced pattern matching for validation
public ValidationResult validatePayment(Payment payment) {
return switch (payment) {
case CreditCardPayment(var cardNumber, var amount, var expiry)
when amount.compareTo(BigDecimal.ZERO) <= 0 ->
ValidationResult.invalid("Amount must be positive");
case CreditCardPayment(var cardNumber, var amount, var expiry)
when !isValidCardNumber(cardNumber) ->
ValidationResult.invalid("Invalid card number");
case CreditCardPayment(var cardNumber, var amount, var expiry)
when expiry.isBefore(LocalDate.now()) ->
ValidationResult.invalid("Card expired");
case PayPalPayment(var email, var amount)
when !isValidEmail(email) ->
ValidationResult.invalid("Invalid email format");
case Payment p when p.getAmount().compareTo(MAX_AMOUNT) > 0 ->
ValidationResult.invalid("Amount exceeds maximum limit");
default -> ValidationResult.valid();
};
}
}
Records: Beyond Simple DTOs
Records are perfect for immutable data classes, but they can do much more:
// Basic Record for API responses
public record UserResponse(
Long id,
String username,
String email,
LocalDateTime createdAt,
UserProfile profile
) {
// Custom constructor with validation
public UserResponse {
Objects.requireNonNull(username, "Username cannot be null");
Objects.requireNonNull(email, "Email cannot be null");
if (username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}
}
// Static factory methods
public static UserResponse fromEntity(User user) {
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getCreatedAt(),
user.getProfile()
);
}
// Computed properties
public String displayName() {
return profile != null && profile.fullName() != null
? profile.fullName()
: username;
}
// Business logic methods
public boolean isRecent() {
return createdAt.isAfter(LocalDateTime.now().minusDays(30));
}
}
// Records for configuration
public record DatabaseConfig(
String url,
String username,
String password,
int maxPoolSize,
Duration connectionTimeout
) {
public DatabaseConfig {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("Max pool size must be positive");
}
if (connectionTimeout.isNegative()) {
throw new IllegalArgumentException("Connection timeout cannot be negative");
}
}
// Default configuration
public static DatabaseConfig defaultConfig() {
return new DatabaseConfig(
"jdbc:postgresql://localhost:5432/myapp",
"app_user",
"password",
10,
Duration.ofSeconds(30)
);
}
}
// Records for complex data structures
public record PaginatedResponse<T>(
List<T> content,
int page,
int size,
long totalElements,
boolean hasNext,
boolean hasPrevious
) {
public static <T> PaginatedResponse<T> of(Page<T> page) {
return new PaginatedResponse<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.hasNext(),
page.hasPrevious()
);
}
public int totalPages() {
return (int) Math.ceil((double) totalElements / size);
}
}
Spring Boot Best Practices That Most Developers Miss
1. Proper Configuration Management
Most developers don't leverage Spring Boot's powerful configuration features:
// Instead of scattered @Value annotations
@Component
public class EmailService {
@Value("${email.smtp.host}")
private String smtpHost;
@Value("${email.smtp.port}")
private int smtpPort;
@Value("${email.from}")
private String fromAddress;
// ... rest of the messy code
}
// Use Configuration Properties (BEST PRACTICE)
@ConfigurationProperties(prefix = "email")
@Validated
public record EmailProperties(
@NotBlank String fromAddress,
@Valid SmtpConfig smtp,
@Valid RetryConfig retry,
boolean enabled
) {
public record SmtpConfig(
@NotBlank String host,
@Min(1) @Max(65535) int port,
boolean auth,
boolean startTls,
String username,
String password
) {}
public record RetryConfig(
@Min(1) int maxAttempts,
@NotNull Duration initialDelay,
@DecimalMin("1.1") double multiplier
) {}
}
@Service
@RequiredArgsConstructor
public class EmailService {
private final EmailProperties emailProperties;
private final JavaMailSender mailSender;
@Retryable(
retryFor = MailException.class,
maxAttemptsExpression = "#{@emailProperties.retry().maxAttempts()}",
backoff = @Backoff(
delayExpression = "#{@emailProperties.retry().initialDelay().toMillis()}",
multiplierExpression = "#{@emailProperties.retry().multiplier()}"
)
)
public void sendEmail(String to, String subject, String body) {
if (!emailProperties.enabled()) {
log.info("Email sending is disabled");
return;
}
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(emailProperties.fromAddress());
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
log.info("Email sent successfully to {}", to);
}
}
2. Advanced Exception Handling
Create a robust error handling system:
// Global exception handler with proper error responses
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(ValidationException ex) {
log.warn("Validation error: {}", ex.getMessage());
return ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message(ex.getMessage())
.path(getCurrentPath())
.validationErrors(ex.getFieldErrors())
.build();
}
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(EntityNotFoundException ex) {
log.warn("Entity not found: {}", ex.getMessage());
return ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Resource Not Found")
.message(ex.getMessage())
.path(getCurrentPath())
.build();
}
@ExceptionHandler(OptimisticLockingFailureException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleOptimisticLocking(OptimisticLockingFailureException ex) {
log.warn("Optimistic locking failure: {}", ex.getMessage());
return ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.CONFLICT.value())
.error("Concurrent Modification")
.message("The resource was modified by another user. Please refresh and try again.")
.path(getCurrentPath())
.build();
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
String errorId = UUID.randomUUID().toString();
log.error("Unexpected error [{}]: ", errorId, ex);
return ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("An unexpected error occurred. Error ID: " + errorId)
.path(getCurrentPath())
.build();
}
private String getCurrentPath() {
return RequestContextHolder.currentRequestAttributes()
.getAttribute(RequestAttributes.REFERENCE_REQUEST, RequestAttributes.SCOPE_REQUEST)
.toString();
}
}
// Custom error response record
public record ErrorResponse(
Instant timestamp,
int status,
String error,
String message,
String path,
Map<String, String> validationErrors
) {
public static ErrorResponseBuilder builder() {
return new ErrorResponseBuilder();
}
public static class ErrorResponseBuilder {
// Builder pattern implementation
}
}
3. Performance Optimization with Caching
Implement intelligent caching strategies:
@Configuration
@EnableCaching
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true", matchIfMissing = true)
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
private Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterAccess(Duration.ofMinutes(10))
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats();
}
@Bean
@ConditionalOnProperty(name = "cache.redis.enabled", havingValue = "true")
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public Optional<UserResponse> findById(Long id) {
log.debug("Fetching user with id: {}", id);
return userRepository.findById(id)
.map(userMapper::toResponse);
}
@Cacheable(value = "user-profiles", key = "#username")
public UserProfile getUserProfile(String username) {
return userRepository.findByUsername(username)
.map(this::buildUserProfile)
.orElse(null);
}
@CacheEvict(value = {"users", "user-profiles"}, key = "#user.id")
public UserResponse updateUser(User user) {
User savedUser = userRepository.save(user);
log.info("Updated user: {}", savedUser.getId());
return userMapper.toResponse(savedUser);
}
@Caching(evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "user-profiles", allEntries = true)
})
public void deleteUser(Long id) {
userRepository.deleteById(id);
log.info("Deleted user: {}", id);
}
// Cache warming strategy
@EventListener
@Async
public void handleApplicationReady(ApplicationReadyEvent event) {
log.info("Warming up user cache...");
userRepository.findTopActiveUsers(100)
.forEach(user -> {
// Pre-load frequently accessed users
findById(user.getId());
getUserProfile(user.getUsername());
});
log.info("User cache warmed up successfully");
}
}
4. Modern Security Patterns
Implement comprehensive security:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/{id}").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/users").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasRole("ACTUATOR")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.jwtDecoder(jwtDecoder())))
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
.preload(true)))
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation("https://your-auth-server.com");
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of(
"https://your-frontend-domain.com",
"https://*.your-domain.com"
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
// Method-level security
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public UserResponse findById(Long id) {
// Method implementation
}
@PreAuthorize("@userService.isOwnerOrAdmin(#id, authentication.principal.id)")
public void deleteUser(Long id) {
// Method implementation
}
public boolean isOwnerOrAdmin(Long userId, Long currentUserId) {
return userId.equals(currentUserId) ||
SecurityContextHolder.getContext()
.getAuthentication()
.getAuthorities()
.stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
}
}
5. Comprehensive Observability
Implement proper monitoring and metrics:
@Configuration
@ConditionalOnProperty(name = "management.metrics.enabled", havingValue = "true", matchIfMissing = true)
public class ObservabilityConfig {
@Bean
@ConditionalOnMissingBean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags(
"application", "modern-java-app",
"version", getClass().getPackage().getImplementationVersion(),
"environment", getCurrentEnvironment()
);
}
@Bean
public HealthIndicator databaseHealthIndicator(DataSource dataSource) {
return new DataSourceHealthIndicator(dataSource, "SELECT 1");
}
@Bean
public HealthIndicator customHealthIndicator() {
return new AbstractHealthIndicator() {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// Custom health checks
boolean externalServiceAvailable = checkExternalService();
if (externalServiceAvailable) {
builder.up()
.withDetail("external-service", "Available")
.withDetail("timestamp", Instant.now());
} else {
builder.down()
.withDetail("external-service", "Unavailable")
.withDetail("timestamp", Instant.now());
}
}
};
}
}
@RestController
@RequiredArgsConstructor
@Timed(name = "api.requests", description = "API request metrics")
public class UserController {
private final UserService userService;
private final MeterRegistry meterRegistry;
@GetMapping("/users/{id}")
@Timed(name = "api.users.get", description = "Get user by ID")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
Counter.Sample sample = Timer.start(meterRegistry);
try {
UserResponse user = userService.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
meterRegistry.counter("api.users.get.success").increment();
return ResponseEntity.ok(user);
} catch (EntityNotFoundException e) {
meterRegistry.counter("api.users.get.not_found").increment();
throw e;
} catch (Exception e) {
meterRegistry.counter("api.users.get.error").increment();
throw e;
} finally {
sample.stop(Timer.builder("api.users.get.duration")
.description("User retrieval duration")
.register(meterRegistry));
}
}
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse user = userService.createUser(request);
meterRegistry.counter("users.created").increment();
meterRegistry.gauge("users.total", userService.getTotalUserCount());
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
}
Modern Architecture Patterns
Clean Architecture with Spring Boot
// Domain layer - Pure business logic
public class User {
private final UserId id;
private final Username username;
private final Email email;
private UserStatus status;
private LocalDateTime lastLoginAt;
public User(UserId id, Username username, Email email) {
this.id = Objects.requireNonNull(id);
this.username = Objects.requireNonNull(username);
this.email = Objects.requireNonNull(email);
this.status = UserStatus.ACTIVE;
}
public void login() {
if (status != UserStatus.ACTIVE) {
throw new UserNotActiveException("User is not active");
}
this.lastLoginAt = LocalDateTime.now();
}
public void deactivate() {
this.status = UserStatus.INACTIVE;
}
public boolean canPerformAction(Action action) {
return status == UserStatus.ACTIVE &&
hasPermissionFor(action);
}
// Domain events
public List<DomainEvent> getUncommittedEvents() {
return List.copyOf(uncommittedEvents);
}
}
// Application layer - Use cases
@UseCase
@RequiredArgsConstructor
@Transactional
public class CreateUserUseCase {
private final UserRepository userRepository;
private final EmailService emailService;
private final DomainEventPublisher eventPublisher;
public UserId execute(CreateUserCommand command) {
// Validate business rules
if (userRepository.existsByEmail(command.email())) {
throw new EmailAlreadyExistsException("Email already exists");
}
if (userRepository.existsByUsername(command.username())) {
throw new UsernameAlreadyExistsException("Username already exists");
}
// Create domain object
User user = new User(
userRepository.nextId(),
new Username(command.username()),
new Email(command.email())
);
// Persist
userRepository.save(user);
// Publish domain events
eventPublisher.publishAll(user.getUncommittedEvents());
// Side effects
emailService.sendWelcomeEmail(user.getEmail());
return user.getId();
}
}
// Infrastructure layer - Framework concerns
@Repository
@RequiredArgsConstructor
public class JpaUserRepository implements UserRepository {
private final SpringDataUserRepository springDataRepository;
private final UserMapper userMapper;
@Override
public Optional<User> findById(UserId id) {
return springDataRepository.findById(id.getValue())
.map(userMapper::toDomain);
}
@Override
public void save(User user) {
UserEntity entity = userMapper.toEntity(user);
springDataRepository.save(entity);
}
@Override
public UserId nextId() {
return new UserId(UUID.randomUUID());
}
}
Performance Monitoring and Optimization
Application Performance Monitoring
@Component
@RequiredArgsConstructor
@Slf4j
public class PerformanceMonitor {
private final MeterRegistry meterRegistry;
@EventListener
public void handleSlowRequest(SlowRequestEvent event) {
Timer.Sample sample = Timer.start(meterRegistry);
meterRegistry.counter("slow.requests",
"endpoint", event.getEndpoint(),
"method", event.getMethod()
).increment();
if (event.getDuration().toMillis() > 5000) {
log.warn("Very slow request detected: {} {} took {}ms",
event.getMethod(),
event.getEndpoint(),
event.getDuration().toMillis());
meterRegistry.counter("very.slow.requests").increment();
}
sample.stop(Timer.builder("request.duration")
.description("Request processing time")
.tag("endpoint", event.getEndpoint())
.register(meterRegistry));
}
@Scheduled(fixedRate = 60000) // Every minute
public void reportMetrics() {
// JVM metrics
long usedMemory = ManagementFactory.getMemoryMXBean()
.getHeapMemoryUsage().getUsed();
long maxMemory = ManagementFactory.getMemoryMXBean()
.getHeapMemoryUsage().getMax();
meterRegistry.gauge("jvm.memory.used", usedMemory);
meterRegistry.gauge("jvm.memory.usage.percentage",
(double) usedMemory / maxMemory * 100);
// Connection pool metrics
HikariDataSource dataSource = getHikariDataSource();
if (dataSource != null) {
meterRegistry.gauge("connection.pool.active",
dataSource.getHikariPoolMXBean().getActiveConnections());
meterRegistry.gauge("connection.pool.idle",
dataSource.getHikariPoolMXBean().getIdleConnections());
}
}
}
Modern Testing Strategies
Comprehensive Testing Approach
// Unit Tests with modern techniques
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUser_WhenValidData() {
// Given
CreateUserRequest request = new CreateUserRequest(
"john_doe",
"john@example.com",
"John Doe"
);
User savedUser = User.builder()
.id(1L)
.username("john_doe")
.email("john@example.com")
.build();
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.existsByUsername(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
UserResponse result = userService.createUser(request);
// Then
assertThat(result).isNotNull();
assertThat(result.username()).isEqualTo("john_doe");
assertThat(result.email()).isEqualTo("john@example.com");
verify(emailService).sendWelcomeEmail("john@example.com");
verify(userRepository).save(argThat(user ->
user.getUsername().equals("john_doe") &&
user.getEmail().equals("john@example.com")
));
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "a", "toolongusernamethatexceedslimit"})
void shouldThrowException_WhenInvalidUsername(String username) {
// Given
CreateUserRequest request = new CreateUserRequest(
username,
"john@example.com",
"John Doe"
);
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("Invalid username");
}
}
// Integration Tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void shouldCreateAndRetrieveUser() {
// Given
CreateUserRequest request = new CreateUserRequest(
"integration_test",
"test@integration.com",
"Integration Test"
);
// When - Create user
ResponseEntity<UserResponse> createResponse = restTemplate.postForEntity(
"/api/users",
request,
UserResponse.class
);
// Then - Verify creation
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
Long userId = createResponse.getBody().id();
// When - Retrieve user
ResponseEntity<UserResponse> getResponse = restTemplate.getForEntity(
"/api/users/" + userId,
UserResponse.class
);
// Then - Verify retrieval
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().username()).isEqualTo("integration_test");
// Verify database state
Optional<User> userInDb = userRepository.findById(userId);
assertThat(userInDb).isPresent();
assertThat(userInDb.get().getUsername()).isEqualTo("integration_test");
}
}
Deployment and Production Best Practices
Docker Configuration
# Multi-stage Docker build for optimal image size
FROM openjdk:21-jdk-slim as build
WORKDIR /app
COPY . .
# Build application
RUN ./mvnw clean package -DskipTests
FROM openjdk:21-jre-slim
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy application jar
COPY --from=build /app/target/*.jar app.jar
# Change ownership to non-root user
RUN chown appuser:appuser app.jar
USER appuser
# Configure JVM for containerized environments
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-Dspring.profiles.active=prod"
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Production Configuration
# application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: none
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
batch_size: 25
order_inserts: true
order_updates: true
cache:
redis:
enabled: true
time-to-live: 30m
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${JWT_ISSUER_URI}
logging:
level:
com.yourcompany: INFO
org.springframework.security: WARN
org.hibernate.SQL: WARN
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n"
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
metrics:
export:
prometheus:
enabled: true
Conclusion and Future of Java Development
Java continues to evolve rapidly, and staying current with these modern features and best practices is crucial for building maintainable, performant applications. The combination of Java 21's powerful features with Spring Boot's mature ecosystem provides everything you need to build world-class applications.
Key Takeaways
Throughout this comprehensive guide, you've learned:
- Virtual Threads revolutionize how we handle concurrency in Java applications
- Pattern Matching makes code more readable and less error-prone
- Records are powerful beyond simple DTOs
- Spring Boot best practices that most developers overlook
- Modern security patterns for production applications
- Performance optimization techniques for high-scale applications
- Comprehensive testing strategies for reliable code
- Production-ready deployment configurations
What's Coming Next in Java
Looking ahead to future Java releases, we can expect:
- Project Loom enhancements for even better virtual thread performance
- Project Panama for improved native interoperability
- Pattern Matching for switch expressions and instanceof
- Value Types for more efficient object representations
- Improved garbage collection algorithms
Next Steps for Your Journey
To continue improving your Java skills:
- Experiment with Virtual Threads in your current projects
- Refactor existing code to use Pattern Matching and Records
- Implement comprehensive observability in your applications
- Practice Test-Driven Development with modern testing tools
- Stay updated with the latest Java Enhancement Proposals (JEPs)
- Contribute to open-source Spring Boot projects
The Java ecosystem has never been more exciting, and with these modern techniques, you're well-equipped to build the next generation of high-performance, scalable applications.
What's your experience with these modern Java features? Have you implemented Virtual Threads in production? Share your thoughts and experiences in the comments below!
Happy coding with modern Java! ☕🚀