Java 21 officially graduates Virtual Threads (JEP 444) as a production-ready feature. Unlike platform (OS) threads, virtual threads are lightweight threads managed by the JVM itself — you can create millions of them without worrying about memory or scheduling overhead.
The API is deliberately familiar. Here are three ways to spin up a virtual thread:
// 1. Thread.ofVirtual() Thread vt = Thread.ofVirtual() .name("vt-worker") .start(() -> System.out.println("Hello!")); // 2. Executors.newVirtualThreadPerTaskExecutor() try (var exec = Executors .newVirtualThreadPerTaskExecutor()) { exec.submit(() -> processRequest()); } // 3. Thread.startVirtualThread() Thread.startVirtualThread(() -> fetchData());
Spring Boot 3.2 added first-class support. A single property enables virtual threads for the embedded Tomcat:
# application.properties spring.threads.virtual.enabled=true
That's it. Your Spring MVC controllers now handle each request on a virtual thread — no WebFlux, no reactive operators, no callback hell.
In benchmarks simulating 10,000 concurrent HTTP connections with I/O blocking (database + external API calls):
Virtual threads excel at I/O-bound workloads. For CPU-bound tasks (image processing, encryption), platform threads are still the right choice — you don't want millions of threads fighting for CPU cores.
ReentrantLock instead.Virtual threads are arguably the most impactful Java feature in a decade. They let you write simple, blocking-style code and get near-reactive throughput — the best of both worlds. Migrate today by adding a single property to your Spring Boot 3.2+ app.
Spring Security 6 (shipped with Spring Boot 3) dropped the deprecated WebSecurityConfigurerAdapter. You now configure security entirely through SecurityFilterChain beans — a more composable and testable approach that makes your security config a proper Spring bean.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.3</version> </dependency>
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain( HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s .sessionCreationPolicy(STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }
public String generateToken(UserDetails userDetails) { return Jwts.builder() .subject(userDetails.getUsername()) .issuedAt(new Date()) .expiration(new Date( System.currentTimeMillis() + 86_400_000)) .signWith(getSigningKey()) .compact(); }
@Component public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws Exception { String header = req.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); String username = jwtService.extractUsername(token); } chain.doFilter(req, res); } }
# 1. Login and get token curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"pass"}' # 2. Call protected endpoint curl http://localhost:8080/api/products \ -H "Authorization: Bearer <your-token>"
Spring Security 6 modernises the configuration model significantly. The move to SecurityFilterChain beans makes your security setup more testable, composable, and easier to understand. Combine it with JWT for a robust stateless REST API.
Imagine loading 100 Orders, each with a list of OrderItems. Without care, Hibernate fires 1 query for orders, then 100 more to fetch each order's items — 101 queries total. On production data sets this is catastrophic for performance.
// This triggers N+1 silently! List<Order> orders = orderRepo.findAll(); orders.forEach(o -> o.getItems().size()); // lazy load — 1 query each
First, enable statistics to count queries per request:
# application.properties spring.jpa.properties.hibernate .generate_statistics=true logging.level.org.hibernate.stat=DEBUG
The log will show StatisticsImpl entries revealing total query counts. Any number far above expected confirms a problem.
@Query("SELECT o FROM Order o " + "JOIN FETCH o.items") List<Order> findAllWithItems();
@EntityGraph(attributePaths = {"items"}) List<Order> findAll();
@BatchSize(size = 25) @OneToMany(mappedBy = "order") private List<OrderItem> items;
@BatchSize as a fallback. Never use FetchType.EAGER globally — it solves N+1 but creates worse problems.The N+1 problem is one of the most common Hibernate pitfalls. Enable statistics in development, profile your queries, identify the hotspots, and apply the right fix. Always measure before and after to confirm improvement in production-like conditions.