Back to Blog
Modern Java Development: Mastering Java 21+ Features and Spring Boot Best Practices in 2025
Java Development

Modern Java Development: Mastering Java 21+ Features and Spring Boot Best Practices in 2025

Discover the latest Java 21+ features including Virtual Threads, Pattern Matching, and Records. Learn advanced Spring Boot best practices, performance optimizations, and modern architectural patterns that every Java developer should know.

36 viewsJuly 1, 202516 min readRabi
javaspring-bootvirtual-threadsperformancebest-practicesjava21microservices

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:

  1. Virtual Threads revolutionize how we handle concurrency in Java applications
  2. Pattern Matching makes code more readable and less error-prone
  3. Records are powerful beyond simple DTOs
  4. Spring Boot best practices that most developers overlook
  5. Modern security patterns for production applications
  6. Performance optimization techniques for high-scale applications
  7. Comprehensive testing strategies for reliable code
  8. 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:

  1. Experiment with Virtual Threads in your current projects
  2. Refactor existing code to use Pattern Matching and Records
  3. Implement comprehensive observability in your applications
  4. Practice Test-Driven Development with modern testing tools
  5. Stay updated with the latest Java Enhancement Proposals (JEPs)
  6. 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! ☕🚀

Liked the blog?

Share it with your friends and help them learn something new!