From Legacy to Modern: Refactoring Java EE Monoliths into Quarkus Microservices
A Practical Guide to Breaking Monoliths with Quarkus (and Staying Sane)
Let’s face it: Many enterprises still run on Java EE monoliths.
These applications are often massive, battle-tested, and… hard to touch.
But business moves faster now. You’re asked for:
- Faster release cycles
- Cloud-native readiness
- Lower infrastructure costs
- Containerization
And here’s where things get tricky:
How do you safely refactor a Java EE monolith into modern Quarkus microservices without breaking everything?
This guide walks you through step-by-step strategies for doing just that.
We’ll cover:
- How to extract services carefully
- Mapping CDI to Spring or Quarkus DI
- Dockerizing legacy code safely
- Lessons from real migrations
No silver bullets—just realistic advice for developers working in the trenches.
Why Quarkus?
Quarkus is a modern, Kubernetes-friendly Java stack designed for:
- Fast boot times
- Low memory usage (GraalVM native images optional)
- Developer productivity (hot reload, dev services)
It’s a great fit for moving legacy Java EE workloads to containers and the cloud.
Quarkus supports both:
- Jakarta EE standards (JAX-RS, CDI, JPA)
- MicroProfile and reactive programming with Vert.x
- Spring compatibility mode (more on that later)
Step 1: Understand Your Monolith
Before splitting anything, map out your monolith’s structure:
| What to Look For | Why It Matters |
|---|---|
| Business Domains (e.g., Billing, Customer, Orders) | Candidate services |
| Shared libraries or utilities | Identify tight coupling |
| Database schema ownership | Define data boundaries |
| Session state usage | Statelessness is key for microservices |
| Legacy APIs (JAX-RS, SOAP) | Determine what can be reused |
Step 2: Identify the First Service to Extract
Don’t try to “microservice all the things” at once. Pick one service to extract first.
Criteria for a Good Candidate:
- Business boundary is clear (e.g., Inventory, User Management)
- Minimal dependencies on other modules
- Low-risk for failures during the split
Step 3: Migrate Code to Quarkus
Quarkus supports both CDI and a Spring-like API layer.
If you’re coming from Java EE, you’ll mostly use CDI annotations.
Mapping Java EE to Quarkus
| Java EE | Quarkus Equivalent |
|---|---|
@Stateless, @Singleton | @ApplicationScoped, @Singleton (Quarkus CDI) |
@Inject | @Inject (Quarkus CDI works the same) |
@EJB | Use @Inject with plain beans (EJB is not needed anymore) |
@Path, @GET, @POST | Same JAX-RS annotations (Quarkus supports JAX-RS natively) |
@PersistenceContext | Use @Inject EntityManager |
| JTA transactions | @Transactional (Quarkus has ArC support) |
Example Migration
Java EE Style:
@Stateless
@Path("/users")
public class UserService {
@PersistenceContext
EntityManager em;
@GET
public List<User> getUsers() {
return em.createQuery("SELECT u FROM User u").getResultList();
}
}
Quarkus Version:
@Path("/users")
@ApplicationScoped
@Transactional
public class UserService {
@Inject
EntityManager em;
@GET
public List<User> getUsers() {
return em.createQuery("SELECT u FROM User u").getResultList();
}
}
Step 4: Handle Configuration and Environment
In Java EE, you might have used web.xml or application servers for configuration.
In Quarkus, switch to application.properties or application.yaml:
quarkus.datasource.db-kind=postgresql quarkus.datasource.username=dbuser quarkus.datasource.password=secret
Quarkus also supports:
- MicroProfile Config
- Kubernetes/OpenShift environment variables
Step 5: Dockerize the Service
Quarkus makes Dockerization easy.
Use quarkus.container-image.docker extension or create a simple Dockerfile:
FROM quay.io/quarkus/ubi-quarkus-native-image:latest COPY target/*-runner /application CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
Or for JVM mode:
FROM eclipse-temurin:21-jdk COPY target/quarkus-app/ /app/ CMD ["java", "-jar", "/app/quarkus-run.jar"]
Step 6: Deploy and Test
Run the service locally with:
./mvnw compile quarkus:dev
Deploy to Kubernetes or OpenShift using:
./mvnw clean package -Dquarkus.kubernetes.deploy=true
Lessons Learned from Real Migrations
1️⃣ Don’t Over-Split
Splitting a monolith into too many microservices too fast creates:
- Network overhead
- Complex deployments
- Service sprawl
Start small. Grow as needed.
2️⃣ Use APIs to Extract, Not Rewrite Everything
Expose parts of the monolith as APIs (REST or gRPC) before breaking them apart physically.
This allows gradual separation.
3️⃣ Watch for Database Coupling
If your services share the same database schema, you haven’t truly decoupled.
Options:
- Use database views or proxies temporarily
- Move toward dedicated schemas per service over time
4️⃣ Beware of Stateful Logic
Java EE apps often rely on:
- HTTP sessions
- EJB session beans
Microservices should be stateless. Move state to:
- Redis or distributed caches
- Databases
5️⃣ Leverage Quarkus Dev Mode
Quarkus provides hot reload and dev services. Use them for fast feedback.
./mvnw quarkus:dev
Useful Tools and Links
- Quarkus Getting Started Guide
https://quarkus.io/get-started/ - Quarkus and Docker Guide
https://quarkus.io/guides/container-image - Migrating from Java EE to Quarkus (Red Hat Blog)
https://developers.redhat.com/articles/migrate-java-ee-quarkus - CDI and Dependency Injection in Quarkus
https://quarkus.io/guides/cdi-reference
Final Thoughts
Migrating from a Java EE monolith to Quarkus microservices is doable—but it’s not trivial.
- Start small
- Extract APIs gradually
- Dockerize as you go
- Take lessons from real-world cases, not just tutorials
Modernization is a marathon, not a sprint.
Quarkus can help you run it faster—but only if you pace yourself.

