f in x
Modern Java and JVM — Virtual Threads, Spring Boot 3, and Code That Scales
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Modern Java and JVM — Virtual Threads, Spring Boot 3, and Code That Scales

[2026-06-22] Author: Ing. Calogero Bono

Your backend project is growing, but your Java code still feels like 2014? You're not alone. We see it every day in projects that come to us: enterprise apps with outdated indexes, threads blocking on I/O, builds that take minutes. Java is no longer the slow, verbose platform you remember. Since Java 17 LTS, the JVM has become an ecosystem that can compete with Go or Rust for performance, and with Python for development speed — if you know what to use. At Meteora Web, we work daily with the modern Java stack on real projects: SaaS, REST APIs, proprietary platforms. In this pillar page you won't find academic theory, but concrete choices about virtual threads, Spring Boot 3, JPA, Docker, and Kotlin, with the numbers and logic that matter for software that produces value. Let's start with the problem: is your Java truly modern?

How did Java versions change from 17 to 21?

Until a few years ago, moving from Java 8 to 11 was a huge leap. Today the release cadence is semiannual, but LTS still matters. Java 17 was the turning point: records, sealed classes, pattern matching for instanceof, and a ZGC garbage collector that reduces pauses to milliseconds. Java 21 (LTS 2023) brought virtual threads, which change how we write concurrent code. We adopted Java 21 at release, and the results on web applications are clear: fewer thread pools, less boilerplate code, higher throughput. If you're still on Java 8 or 11, you're leaving performance and productivity on the table.

What changes with virtual threads?

Virtual threads are lightweight threads managed by the JVM, not the OS. You can create millions without exhausting memory. For an I/O-heavy application (API, DB, files), virtual threads eliminate the need for complex thread pools: each request can have its own virtual thread, and the JVM handles scheduling. We tested on one of our Spring Boot 3 projects with 10,000 concurrent requests: the server went from 32 real threads to 1,000 virtual ones with lower CPU usage. The code? Just set spring.threads.virtual.enabled=true in Spring Boot 3.2+. No refactoring needed.

Sponsored Protocol

Records, Sealed Classes, and Pattern Matching

Records replace POJO classes with auto-generated getters, equals, hashCode. We use them for DTOs and value objects: zero boilerplate. Sealed classes restrict subclasses of a hierarchy — useful for domains and events. Pattern matching (on instanceof and switch) makes code more readable and safer, eliminating explicit casts. Example:

// Before
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// After (Java 17+)
if (obj instanceof String s) {
    System.out.println(s.length());
}

Is Spring Boot 3 worth the upgrade from Spring Boot 2?

Spring Boot 3 is based on Spring Framework 6, requiring Java 17+. Key new features: native support for GraalVM (build-time), virtual threads, Jakarta EE 9+ (namespace changes from javax to jakarta), and a new observability module (Micrometer). If you're starting from scratch, Spring Boot 3 is the only choice. If you have an existing Spring Boot 2.x project, migration is straightforward but you need to update all dependencies. We did it for a custom e-commerce project: two days of work to update 30 modules. The payoff? Native images with GraalVM have startup times under 1 second, perfect for Kubernetes. Let's see a complete application.

Build a REST application with Spring Boot 3

Use Spring Initializr (or our custom Maven archetype). Minimal dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation. Here's a modern controller with virtual threads and records:

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @GetMapping
    public List findAll() {
        return service.findAll();
    }

    @PostMapping
    public UserDTO create(@Valid @RequestBody CreateUserRequest request) {
        return service.create(request);
    }
}

public record UserDTO(Long id, String name, String email) {}
public record CreateUserRequest(String name, String email) {}

With virtual threads enabled, each HTTP request runs on a virtual thread. Database queries block the virtual thread, not the real one, so you can increase load without adding real threads.

Sponsored Protocol

Modern Spring Security: JWT and OAuth2 authentication

Security in Spring Boot 3 has changed. The old WebSecurityConfigurerAdapter is deprecated. Use a declarative SecurityFilterChain. For JWT authentication, use spring-security-oauth2-resource-server which natively supports JWT. Minimal configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

For OAuth2 with external providers (Google, GitHub), Spring Security has ready adapters. We integrated OAuth2 into a B2B platform for social login and corporate SSO, with configuration entirely in application.yml.

JPA and Hibernate: how to avoid the N+1 problem and improve performance

JPA is convenient, but if you don't tame it, it kills performance. The N+1 problem: one query for each child entity. Example: load orders and for each order load products. With JPA, by default, collections are lazy, but accessing order.getProducts() triggers a query. The solution? Use explicit fetch join in JPQL:

@Query("SELECT o FROM Order o JOIN FETCH o.products WHERE o.id = :id")
Optional findByIdWithProducts(@Param("id") Long id);

Or use Entity Graph annotation. Another best practice: use Stream for massive queries and avoid List with millions of rows. We optimized a warehouse inventory with JPA: reduced queries from 2000 to 12 using fetch joins and batch size (@BatchSize(size=50)).

Sponsored Protocol

Modern mapping: records and projections

Instead of full entities, use projections with interfaces or records. JPA supports records as query results:

public record OrderSummary(Long id, String customerName, BigDecimal total) {}

@Query("SELECT new com.example.OrderSummary(o.id, c.name, o.total) FROM Order o JOIN o.customer c")
List findSummaries();

This reduces data transfer and memory usage.

Maven vs Gradle: which is the right choice for a Java team today?

Maven is predictable, XML verbose but standard. Gradle is faster, with Groovy or Kotlin DSL, and incremental builds. The answer? It depends on the project. For standard linear builds, Maven is fine. For multi-module projects, complex builds, or CI with GitHub Actions, Gradle has an edge: build cache and parallel execution. We use Gradle with Kotlin DSL for Spring Boot projects because the build file is more readable and supports native Docker configuration. We keep Maven for legacy projects. No absolute winner, but if starting from scratch, Gradle is more modern.

Kotlin for Java developers: is the transition really worth it?

Kotlin is 100% interoperable with Java but reduces boilerplate: data classes, null safety, extension functions, coroutines (alternative to virtual threads). If your team knows Java, switching to Kotlin is painless and leads to more concise code. We wrote an entire social management platform in Kotlin on Spring Boot: lines of code dropped by 40%. However, Kotlin has a learning overhead (DSL, functional constructs) and slightly slower builds. For new projects, we recommend Kotlin if the team is motivated; for existing Java projects, a full refactoring isn't worth it. Use Kotlin for new services and keep Java for consolidated modules.

Reactive programming with Project Reactor and Spring WebFlux

WebFlux is the non-blocking counterpart of Spring MVC. It's based on Project Reactor (Mono/Flux). It's suitable for high-I/O concurrent applications like API gateways or microservices calling many external services. However, with the arrival of virtual threads, the blocking imperative model becomes competitive again. We recommend WebFlux only when you need backpressure or to integrate reactive libraries (e.g., reactive MongoDB driver). Otherwise, use Spring MVC + virtual threads: simpler, more testable.

Sponsored Protocol

Docker with Java: how to optimize JVM images for production

A Docker image with Java can weigh 500 MB if you use the full JDK. Best practices: multi-stage build with JDK for compilation and JRE slim (e.g., Eclipse Temurin or Amazon Corretto). Or native build with GraalVM to reduce to 20 MB. Example of an optimized Dockerfile:

FROM eclipse-temurin:21-alpine AS builder
WORKDIR /app
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY . .
RUN ./mvnw package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

Also, for containers, set -XX:+UseZGC and memory limits with -XX:MaxRAMPercentage=75.0. We reduced containers from 500MB to 120MB with JRE Alpine, and startup time dropped below 3 seconds.

Java Collections advanced: when to use List, Set, Map, and their variants

Most developers use ArrayList and HashMap for everything, but alternatives can solve specific problems:

  • LinkedList: rarely useful (middle insertion is still O(n)). Prefer ArrayDeque for queues.
  • TreeSet/TreeMap: natural ordering, but O(log n). Use only if you need dynamically sorted elements.
  • LinkedHashSet/LinkedHashMap: insertion order guaranteed, useful for LRU caches.
  • ConcurrentHashMap: thread-safe without locking the whole map. We use it for shared caches.
  • CopyOnWriteArrayList: for lists read often, written rarely (e.g., configurations).

Rule of thumb: ArrayList for 90% of cases, HashMap for key-value, and evaluate alternatives only if you measure a bottleneck.

Sponsored Protocol

Testing Java with JUnit 5 and Mockito: tests that give confidence, not just coverage

Testing modern Java code is easier thanks to records and pattern matching. We use JUnit 5 with Mockito for mocking. Best practice: write tests that isolate domain from infrastructure. Example with Spring Boot 3 and Mockito:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository repository;

    @InjectMocks
    UserService service;

    @Test
    void shouldCreateUser() {
        var request = new CreateUserRequest("Mario", "mario@test.com");
        var user = new User(null, "Mario", "mario@test.com");
        when(repository.save(any())).thenReturn(user);

        var result = service.create(request);

        assertThat(result.name()).isEqualTo("Mario");
    }
}

For integration tests, use @SpringBootTest with an in-memory H2 database. Be careful not to load the entire context: use slice annotations like @WebMvcTest for controllers. We have a suite of 400 tests running in 30 seconds with Gradle parallel execution. Testing is not a waste of time: it's the assurance that refactoring won't break everything.

What to do next

If you're developing a new Java project, here are concrete actions to implement today:

  1. Adopt Java 21 LTS — Leave Java 8 behind. Download JDK 21 from Eclipse Temurin or Amazon Corretto.
  2. Use Spring Boot 3.2+ — Enable virtual threads in application.properties.
  3. Review your JPA queries — Find N+1 with spring.jpa.show-sql=true and add fetch joins.
  4. Switch to Gradle if builds are slow, otherwise stick with Maven.
  5. Containerize with JRE Alpine and optimize JVM memory for Docker.
  6. Write tests for every new service. JUnit 5 + Mockito are the minimum.

Do you have a legacy Java project to modernize? We analyze the code, propose a gradual migration plan, and implement it. Contact us for a technical consultation.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()