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:
- Uses virtual threads for all request processing
- Simulates multiple I/O operations concurrently
- Provides metrics on thread usage
- 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:
- Virtual threads eliminate thread pool tuning - create threads liberally
- Blocking I/O becomes acceptable - no need for reactive streams in many cases
- Modern Java features improve code clarity and safety
- 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.