Chapter 1: Modern Spring Boot with Virtual Threads

Introduction

Spring Boot has evolved significantly since its initial release. With Java 21’s virtual threads and Spring Boot 3.x, we can build applications that handle massive concurrency with minimal resource overhead. This chapter explores modern Spring Boot development practices, focusing on virtual threads, reactive patterns, and contemporary Java features.

Learning Objectives

By the end of this chapter, you will:

  • Understand virtual threads and their benefits
  • Configure Spring Boot for optimal concurrency
  • Implement modern Java patterns in Spring applications
  • Set up development environments for AI-enhanced applications

Virtual Threads: A Paradigm Shift

Virtual threads, introduced in Java 21, represent a fundamental shift in how we handle concurrency in JVM applications. Unlike traditional platform threads, virtual threads are:

  • Lightweight: Millions can be created with minimal memory overhead
  • Managed: The JVM handles scheduling and lifecycle
  • Blocking-friendly: Can block without significant performance penalty

Traditional Thread Model vs Virtual Threads

// Traditional approach - limited by platform threads
@RestController
public class TraditionalController {
    
    @GetMapping("/blocking-operation")
    public ResponseEntity<String> blockingOperation() {
        // This blocks a platform thread
        String result = callExternalService();
        return ResponseEntity.ok(result);
    }
    
    private String callExternalService() {
        try {
            Thread.sleep(1000); // Simulates I/O
            return "Result from external service";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "Error";
        }
    }
}
// Virtual threads approach - massive concurrency
@RestController
public class VirtualThreadController {
    
    @GetMapping("/virtual-operation")
    public ResponseEntity<String> virtualOperation() {
        // This runs on a virtual thread - no blocking concerns
        String result = callExternalServiceWithVirtualThread();
        return ResponseEntity.ok(result);
    }
    
    private String callExternalServiceWithVirtualThread() {
        try {
            Thread.sleep(1000); // Still blocks, but on virtual thread
            return "Result from external service on virtual thread: " + 
                   Thread.currentThread();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "Error";
        }
    }
}

Setting Up Modern Spring Boot

Project Structure

spring-boot-app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/app/
│   │   │       ├── Application.java
│   │   │       ├── config/
│   │   │       ├── controller/
│   │   │       ├── service/
│   │   │       └── repository/
│   │   └── resources/
│   │       ├── application.yml
│   │       └── static/
│   └── test/
├── docker-compose.yml
├── Dockerfile
└── pom.xml

Maven Configuration

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>spring-boot-ai-app</artifactId>
    <version>1.0.0</version>
    <name>Spring Boot AI Application</name>
    
    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>
    
    <dependencies>
        <!-- Core Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Security and OAuth2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        
        <!-- Data and Persistence -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Observability -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
        
        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Application Configuration

# application.yml
spring:
  application:
    name: spring-boot-ai-app
  
  # Virtual Threads Configuration
  threads:
    virtual:
      enabled: true
  
  # Database Configuration (H2 for development)
  datasource:
    url: jdbc:h2:file:./data/app-db;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE
    driverClassName: org.h2.Driver
    username: sa
    password: password
  
  h2:
    console:
      enabled: true
      path: /h2-console
  
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
    properties:
      hibernate:
        format_sql: true

# Server Configuration
server:
  port: 8080
  tomcat:
    threads:
      max: 200
      min-spare: 10

# Management and Monitoring
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

# Logging
logging:
  level:
    com.example.app: DEBUG
    org.springframework.security: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"

Virtual Thread Configuration

Enabling Virtual Threads

@SpringBootApplication
@EnableAsync
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    @Bean
    @Primary
    public Executor taskExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
    
    @Bean
    public ThreadPoolTaskExecutor virtualThreadTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(1);
        executor.setQueueCapacity(0);
        executor.setThreadFactory(Thread.ofVirtual().factory());
        executor.setThreadNamePrefix("virtual-");
        executor.initialize();
        return executor;
    }
}

Virtual Thread Service Example

@Service
public class ConcurrentService {
    
    private final RestTemplate restTemplate;
    
    public ConcurrentService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    @Async
    public CompletableFuture<String> processAsync(String input) {
        // This runs on a virtual thread
        return CompletableFuture.supplyAsync(() -> {
            try {
                // Simulate I/O operation
                Thread.sleep(1000);
                return "Processed: " + input + " on " + Thread.currentThread();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "Error processing: " + input;
            }
        });
    }
    
    public List<String> processMultiple(List<String> inputs) {
        List<CompletableFuture<String>> futures = inputs.stream()
            .map(this::processAsync)
            .toList();
        
        return futures.stream()
            .map(CompletableFuture::join)
            .toList();
    }
}

Modern Java Features in Spring Boot

Records for DTOs

// Traditional approach
public class UserDto {
    private String username;
    private String email;
    private boolean enabled;
    
    // Constructors, getters, setters, equals, hashCode, toString...
}

// Modern approach with Records
public record UserDto(
    String username,
    String email,
    boolean enabled
) {}

// Usage in controller
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        UserDto dto = new UserDto(
            user.getUsername(),
            user.getEmail(),
            user.isEnabled()
        );
        return ResponseEntity.ok(dto);
    }
}

Pattern Matching and Switch Expressions

@Service
public class MessageProcessor {
    
    public String processMessage(Object message) {
        return switch (message) {
            case String s -> "Text message: " + s;
            case Integer i -> "Number message: " + i;
            case UserDto(var username, var email, var enabled) -> 
                "User message: " + username + " (" + email + ")";
            case null -> "Null message";
            default -> "Unknown message type: " + message.getClass().getSimpleName();
        };
    }
}

Sealed Classes for Domain Modeling

public sealed interface PaymentMethod 
    permits CreditCard, BankTransfer, DigitalWallet {
}

public record CreditCard(
    String number,
    String expiryDate,
    String holderName
) implements PaymentMethod {}

public record BankTransfer(
    String accountNumber,
    String routingNumber
) implements PaymentMethod {}

public record DigitalWallet(
    String walletId,
    WalletProvider provider
) implements PaymentMethod {}

public enum WalletProvider {
    PAYPAL, APPLE_PAY, GOOGLE_PAY
}

@Service
public class PaymentService {
    
    public PaymentResult processPayment(PaymentMethod method, BigDecimal amount) {
        return switch (method) {
            case CreditCard(var number, var expiry, var holder) -> 
                processCreditCard(number, expiry, holder, amount);
            case BankTransfer(var account, var routing) -> 
                processBankTransfer(account, routing, amount);
            case DigitalWallet(var walletId, var provider) -> 
                processDigitalWallet(walletId, provider, amount);
        };
    }
}

Development Environment Setup

Docker Compose for Development

# docker-compose.dev.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
    volumes:
      - ./data:/app/data
    depends_on:
      - keycloak
      - postgres
    networks:
      - app-network
  
  keycloak:
    image: quay.io/keycloak/keycloak:23.0
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak123
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin123
    command: start-dev
    ports:
      - "8081:8080"
    depends_on:
      - postgres
    networks:
      - app-network
  
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak123
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge

Performance Considerations

Virtual Thread Metrics

@Component
public class VirtualThreadMetrics {
    
    private final MeterRegistry meterRegistry;
    
    public VirtualThreadMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
    
    @EventListener
    public void onApplicationReady(ApplicationReadyEvent event) {
        scheduleMetricsCollection();
    }
    
    @Scheduled(fixedRate = 5000)
    public void collectThreadMetrics() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        
        meterRegistry.gauge("threads.platform.count", 
            threadBean.getThreadCount());
        meterRegistry.gauge("threads.platform.peak", 
            threadBean.getPeakThreadCount());
        
        // Virtual thread metrics (if available)
        if (threadBean instanceof com.sun.management.ThreadMXBean sunBean) {
            meterRegistry.gauge("threads.virtual.count", 
                sunBean.getCurrentThreadAllocatedBytes());
        }
    }
}

Exercise: Building Your First Virtual Thread Application

Task

Create a Spring Boot application that:

  1. Uses virtual threads for all request processing
  2. Simulates multiple I/O operations concurrently
  3. Provides metrics on thread usage
  4. Implements modern Java features (records, sealed classes)

Solution Framework

// Start with this controller
@RestController
public class ConcurrencyDemoController {
    
    private final ConcurrentService concurrentService;
    
    @GetMapping("/concurrent/{count}")
    public ResponseEntity<List<String>> processConcurrent(
            @PathVariable int count) {
        
        List<String> inputs = IntStream.range(0, count)
            .mapToObj(i -> "Task-" + i)
            .toList();
        
        long startTime = System.currentTimeMillis();
        List<String> results = concurrentService.processMultiple(inputs);
        long endTime = System.currentTimeMillis();
        
        results.add("Total time: " + (endTime - startTime) + "ms");
        results.add("Thread type: " + Thread.currentThread());
        
        return ResponseEntity.ok(results);
    }
}

Summary

Virtual threads represent a significant advancement in Java concurrency. Combined with modern Spring Boot features, they enable applications that can handle massive concurrency with simple, imperative code. Key takeaways:

  1. Virtual threads eliminate thread pool tuning - create threads liberally
  2. Blocking I/O becomes acceptable - no need for reactive streams in many cases
  3. Modern Java features improve code clarity and safety
  4. Proper monitoring is essential for understanding performance characteristics

In the next chapter, we’ll explore how to secure these applications using Keycloak for authentication and authorization.

Further Reading