Migrating 70+ Microservices to Java 21: Hard Lessons in Dependency Governance
Standardizing Spring Boot 3, virtual threads, Jakarta namespaces, and SAST/DAST pipelines
#The Technical Debt Ingestion
Over years of fast-paced startup scaling, our microservices drifted across versions. Some ran on Java 8, others on Java 11, with Spring Boot versions ranging from 2.1 to 2.7. This drift made it impossible to share utility libraries, created security review bottlenecks, and blocked us from taking advantage of modern runtime efficiencies.

// Java 21 Upgrade Blueprint: Transitioning traditional blocking JRE threads to virtual threads under Spring Boot 3.
#Standardizing with Maven Parent POMs
To execute the migration without individual service customization, we introduced a centralized **Platform Parent POM**. This parent POM standardizes dependency versions, compiler configurations, security plugins, and container build properties across all services.
<!-- Platform Parent POM snippet standardizing compilation and security -->
<project>
<groupId>com.platform.core</groupId>
<artifactId>platform-parent-pom</artifactId>
<version>2026.1.0</version>
<packaging>pom</packaging>
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.4</spring-boot.version>
<owasp.dependency-check.version>9.0.9</owasp.dependency-check.version>
</properties>
<build>
<plugins>
<!-- Enforce Java 21 compilation -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>#Taming the Jakarta Namespace Transition
The largest breaking change in Spring Boot 3 was the migration from `javax.*` to `jakarta.*` packages. This affected import statements for databases (JPA), validation, and web filters. We automated ~90% of the namespace refactoring using OpenRewrite recipes, running scripts across all service repositories to rewrite imports automatically in bulk.
#Leveraging Virtual Threads (Project Loom)
One of our main business objectives was to improve throughput on I/O-heavy services. Under the thread-per-request model, standard Tomcat servlet containers would block active threads waiting on database queries. By enabling virtual threads in Spring Boot, we unblocked this boundary.
# Enable Java 21 Virtual Threads in Spring Boot 3.2+
spring.threads.virtual.enabled=trueWith virtual threads enabled, memory consumption on high-throughput gateway nodes dropped by 40%, and maximum request capacity increased by over 2.5x under synthetic I/O load tests.