Java Monolith to Microservices

ModernLift · ·12 min read
Part 5 of 9

Decomposing a Java monolith means moving off the heavyweight application server (WebLogic, WebSphere, or JBoss) and extracting bounded domains as independent services, but only where a real scaling, autonomy, or isolation pressure justifies the cost. The work is two layers at once: re-platforming onto a lighter runtime, and cutting the monolith along domain boundaries rather than along the server's accidental structure. The reliable way is incremental. Put a facade in front of the monolith, extract one service at a time, and prove each one equivalent before it carries live traffic. The goal is to relieve named pressures, not to turn everything into a microservice.

Parts 2 through 4 modernized the application in place: a current JDK, a maintained framework, the EJB and Struts seams lifted out. This part takes the deepest move, the one that changes the architecture rather than the version numbers: getting off the heavyweight application server and decomposing the monolith into domain services. It is the most transformative path, and the easiest to overdo, so the whole point of this part is to do it only where a real pressure justifies it.

Two changes at once: the server and the shape

A legacy Java monolith usually carries two coupled problems, and it helps to separate them.

The first is the application server. WebLogic, WebSphere, and JBoss are heavyweight containers: large memory footprints, slow start-up, and, for the commercial servers, licensing that is a standing tax. They also supply the EJB container, JNDI resources, data sources, and a thick layer of proprietary configuration the application leans on. Re-platforming onto a lighter, current runtime (Open Liberty, Spring Boot, a plain container) sheds that weight and brings the application into a container- and CI/CD-friendly shape.

A point worth stating plainly, because vendors blur it: the app server is usually not an end-of-life emergency. IBM has stated no planned end-of-support date for WebSphere traditional 8.5.5 and 9.0.5 (IBM), and current WebLogic and JBoss/WildFly releases are supported. The honest case for leaving rests on cost, footprint, lock-in, and agility, not on an invented deadline. Our WebSphere migration guide makes that boundary explicit.

The second problem is the shape: a single deployable application where everything depends on everything, so any change is risky and no part can scale or deploy on its own. That is the problem decomposition addresses, and it is a different problem from the server, even though they usually travel together.

Decompose only under real pressure

Decomposition is genuinely useful, and genuinely overprescribed. The discipline that keeps it honest is the one our monolith-to-microservices series is built on: extract a service only where you can name the pressure it relieves. There are three legitimate pressures, and they are measurable:

  • Independent scaling. One part of the system (a pricing engine, a document generator, a batch path) consumes most of the compute and forces you to scale the whole monolith to feed it.
  • Team autonomy. Several teams are blocked on one shared release train and can’t deploy without coordinating.
  • Fault isolation. One component’s failure must not be allowed to take down the others.

If you can’t name one of those, the right move is usually not microservices. It is a modular application on a modern runtime, the design discipline of clear boundaries without the network and operational cost of distributed services. Microservices convert a code problem into a distributed-systems problem: network latency, partial failure, distributed data, and the operational overhead of many services. That tax is worth paying for a named pressure and wasteful without one. Over-decomposing produces the distributed monolith, the worst of both worlds.

The clearest way to make the call is to read the signals on each side rather than follow an architecture fashion. Weigh them together, not one row at a time.

SignalPoints toward extracting a servicePoints toward staying a modular monolith
ScalingOne domain’s load forces you to scale the whole appLoad is even, and scaling the whole app is fine
Team structureSeveral teams are blocked on one release trainOne team, or teams that coordinate cleanly on one deploy
Failure blast radiusOne component’s failure must be contained from the restA single failure domain is acceptable for the workload
Data ownershipA domain can own its data with few shared tablesData is deeply shared and expensive to split apart
Operational maturityYou already run CI/CD, tracing, and on-call for many servicesYou do not yet operate distributed systems in production
Change cadenceA domain changes on its own rhythm, oftenThe whole app tends to change together, rarely

If most of your rows land in the right-hand column, decomposition is buying you cost and risk you don’t need. The honest answer for many Java estates is a modern runtime, clean module boundaries, and a small number of services where the left-hand signals are genuine.

Where a Java decomposition goes wrong

The pressures above are the reasons to decompose. These are the ways decomposition fails, and every one of them is more likely on a legacy Java estate than on a greenfield design. Knowing them is how you avoid trading a monolith you understand for a distributed system you don’t.

  • The distributed monolith. Services that still have to be deployed together, or that break each other on every change, keep all the run-time cost of the network and lose the one benefit that justified it. This is the usual result of cutting along the wrong seam: the services are separate on the diagram and coupled in practice.
  • A shared database. The quickest path to a distributed monolith is letting two services read and write the same tables. A schema change then ripples across service boundaries, and the “independent” services can’t deploy independently. Each service should own its data, which on a Java estate means unwinding JPA mappings that spread one domain across many tables.
  • Chatty calls across the network. A method call inside the monolith was effectively free. The same call between services crosses the network, so a screen that once made twenty in-process calls now makes twenty round trips, each able to be slow or fail. Boundaries drawn without regard to call patterns turn a fast page into a slow, fragile one. Cut where the traffic between the two sides is naturally thin.
  • Distributed transactions. Inside the monolith, a single database transaction kept ordering, inventory, and billing consistent for free. Split across services, that transaction becomes a saga: a choreography of steps with compensating actions to undo a half-finished operation. Sagas are a legitimate tool and a real cost. If a set of operations must be atomic, that is a strong signal to keep them inside one service rather than split the transaction across the network.
  • Operational overhead arrives on day one. Ten services need ten deployment pipelines, ten sets of dashboards and alerts, service discovery, distributed tracing, and an on-call rotation that can reason about failures spanning services. A team that has never run distributed systems in production pays this cost immediately, whether or not the pressure that justified the split was real.

Cut along the domain, not the server’s structure

When extraction is justified, the highest-leverage decision is where to cut. The trap is to cut along the monolith’s accidental internal structure, the packages and modules that reflect how it grew, often shaped by the app server’s own layering. Cut there and the services stay entangled.

Cut instead along domain boundaries, the business capabilities the application actually performs (ordering, billing, identity, notifications) and the seams between them. This is the domain-driven design work, and on a legacy Java estate it does double duty: mapping the domain is also where the undocumented business rules buried in EJB methods and Struts actions get captured as living specs, so behavior survives the move. A Java monolith is especially prone to one trap here: an overloaded domain object, the Account that means three different things, smeared across the schema by years of JPA mappings. The domain map is what reveals it, and the ownership rules above are what keep the split clean.

The incremental loop, applied to Java

The extraction loop is the series’ standard one, and it is what keeps a server migration and a decomposition from becoming a single catastrophic cutover:

  1. Stand up a facade in front of the monolith. On day one it routes everything to the legacy app on the old server, and nothing has changed.
  2. Extract one bounded domain onto the modern runtime, with its own data, on the current framework and JDK from the earlier parts.
  3. Prove parity. The new service behaves identically to the monolith for the same inputs, demonstrated with shadow traffic, including the data and failure behavior the EJB container used to manage.
  4. Shift traffic gradually, rollback one flag away.
  5. Retire the slice from the monolith and the old server. Repeat.

A first slice should be chosen for clarity and low blast radius, to prove the loop, not for the headline pressure. Once the loop is proven, sequence the rest by value. The full mechanics are in the monolith-to-microservices playbook. This part is its Java-stack application.

The reversal worth keeping: leaving the app server and breaking up the monolith are big moves, but they are only safe when they are made small: one domain, one runtime, one proven slice at a time.

Knowing when not to decompose

The most important decision in this part is knowing when not to decompose. A stable Java monolith on a supported app server, under no scaling, autonomy, or isolation pressure, is often best served by a re-platform onto a lighter runtime, or by being left where it is, rather than carved into services it doesn’t need. Many sound Java modernizations end with a handful of extracted services and the rest a clean, modular application on a modern JDK and framework. The migration succeeds when the named pressures are relieved, not when an architecture diagram is satisfied. Decomposing further than the pressure demands just buys operational cost and the risk of a distributed monolith.

Where this leads

The backend is now on a modern runtime and cut where it needed cutting. But the application a user actually touches is the frontend, and on a legacy Java estate that is often a JSP/Struts UI or an AngularJS single-page app that has itself reached end of life. Part 6, AngularJS & Legacy Frontend Modernization, takes the layer in front of the Java backend: why a dead frontend framework is its own risk, and how to modernize it incrementally alongside the services it talks to.

Frequently asked questions

Do you have to leave WebLogic, WebSphere, or JBoss to modernize a Java application?
Not always. IBM has stated no planned end-of-support date for WebSphere traditional 8.5.5 and 9.0.5 (IBM), and current WebLogic and JBoss/WildFly releases are supported, so a stable app server is not an emergency by itself. The pressure usually comes from the runtime and framework underneath, and from the cost, footprint, and lock-in of the heavyweight server. Leaving it is a transformative move worth making when those pressures are real, not a deadline forced by a vendor cutoff.
Should a Java monolith become microservices?
Only where a specific pressure justifies it: a component that must scale independently, teams blocked on one release train, or a failure that must be isolated. Microservices trade build-time simplicity for run-time complexity. Network calls, distributed data, and operational overhead all arrive on day one. Many successful Java modernizations extract a handful of services where the pressure is real and leave the rest as a clean, modular application on a modern runtime. The goal is the pressure relieved, not a target count of services.
How do you decompose a Java monolith without a big-bang cutover?
Put a facade in front of the monolith and extract one bounded domain at a time. Build the service on the modern runtime with its own data, prove it behaves identically to the monolith before it carries live traffic, then shift traffic gradually with rollback ready and retire that slice from the monolith. The application empties of responsibility piece by piece, the business never waits on a single cutover, and you can stop when the pressures that justified the work are gone.
How do we pick the first service to extract from a Java monolith?
Pick the first slice for clarity and low blast radius, not for the biggest pressure. A good first candidate has a clean domain boundary, few callers, its own data with little sharing, and behavior you can verify against the monolith. Read-only or read-mostly paths are ideal early, because a mistake there cannot corrupt live state. The point of the first extraction is to prove the facade, the data split, and the parity harness work end to end. Once that loop is trustworthy, sequence the rest by the pressure each service relieves.
Can microservices share a database?
Sharing one database across services is the fastest way to build a distributed monolith. When two services read and write the same tables, a schema change to one can break the other, and you get all the network cost of separate services with none of the independence. Each extracted service should own its data. On a Java estate that usually means untangling a schema that years of JPA mappings have smeared across domains, which is real work, but it is the work that makes a service genuinely independent. Where a hard consistency boundary exists, keep it inside a single service rather than splitting a transaction across the network.
All 9 parts of Java EE & Legacy Java Modernization →