The honest answer most teams don't want to hear: for the majority of legacy system modernization efforts, the right first move is neither a pure monolith nor a full microservices rewrite. It is a disciplined refactor of your existing monolith, followed by selective extraction of services where you have clear, evidence-backed reasons to split. Microservices solve organizational and scaling problems. If you don't have those problems, you are buying operational complexity you will pay for every day without the corresponding benefit.
That said, the correct choice depends on your constraints, not on architectural fashion. Below I'll give you a practical framework for deciding, based on how these systems actually behave in production.
What the monolith vs. microservices debate is really about
Teams frame this as a technical decision. In practice it is an organizational one. A microservices architecture is primarily a way to let independent teams deploy independently. Martin Fowler and James Lewis made this point in their original description of the style: microservices align with "business capabilities" and independently deployable units (martinfowler.com/microservices).
So before you compare technologies, answer this: do you have multiple teams whose release cadences are blocked by each other today? If one team of eight ships a single application, microservices will mostly add latency, failure modes, and on-call burden. If you have forty engineers across six teams all merging into one deployable and tripping over each other, the monolith is now your bottleneck.
A monolith is a deployment unit, not a code-quality judgment. You can have a clean, well-modularized monolith and a tangled, distributed mess. The distributed mess is worse, because now your spaghetti travels over the network.
A decision framework for legacy system modernization
I use five questions to pressure-test which direction a modernization program should take. None of them is about the technology stack.
- Team topology. How many teams, and are their deploys coupled? Independent deployability is the main reason to split.
- Scaling profile. Does one part of the system have a radically different load or resource profile than the rest? Image processing, search, and billing often do.
- Change frequency. Which modules change weekly versus yearly? High-churn, high-risk areas are extraction candidates. Stable code should stay put.
- Failure isolation requirements. Does a failure in one capability need to be contained so it can't take down the whole platform?
- Operational maturity. Do you have the observability, CI/CD, and on-call practices to run a distributed system? If not, that's a prerequisite, not an afterthought.
If your answers lean toward "one team, uniform load, mostly stable, no hard isolation needs, limited ops maturity," modernize the monolith in place. If they lean the other way, extraction is justified, and you should start small.
When a monolith is the right modernization target
Keep or consolidate into a monolith when:
- A single team owns the system end to end.
- Your pain is code quality and testability, not deployment contention.
- Transactional consistency across the domain is central (a modular monolith keeps this simple with local ACID transactions).
- You need to move fast with a small team and cannot staff a platform function.
A modular monolith often delivers 80% of the organizational benefit with a fraction of the operational cost. You enforce module boundaries in code, not over the network.
// Enforce boundaries inside a modular monolith.
// Modules talk through published interfaces, not internal classes.
package com.example.billing;
public interface BillingFacade {
InvoiceId createInvoice(CustomerId customer, List<LineItem> items);
}
// The orders module depends on the interface, never on billing internals.
// This gives you a clean seam you can later extract into a service
// WITHOUT rewriting callers.
That last comment is the strategy. Clean internal seams now make future extraction cheap. You are not choosing monolith or microservices forever. You are choosing a sequence.
When microservices earn their complexity
Extract services when at least one of these is concretely true:
- Independent scaling. A capability needs to scale on a different axis. Example: a report-generation service that spikes at month-end while the rest of the system is flat.
- Independent deployment by separate teams. Releases are blocked by coordination overhead.
- Technology divergence. One capability genuinely needs a different runtime (for example, a Python ML inference service alongside a Java transactional core).
- Fault isolation. A non-critical capability must not be able to exhaust resources the critical path depends on.
Even then, start with the Strangler Fig pattern. You route traffic through a facade, carve out one capability behind it, and gradually redirect calls until the old code is dead. Fowler documented this approach for exactly this situation (martinfowler.com/StranglerFigApplication).
# Strangler Fig routing: new service handles /api/payments,
# everything else still hits the legacy monolith.
location /api/payments/ {
proxy_pass http://payments-service:8080/;
}
location / {
proxy_pass http://legacy-monolith:8080/;
}
This lets you ship incremental value and roll back a single capability, rather than betting the business on a big-bang rewrite. Big-bang rewrites have a long and well-documented record of failure, and I have never seen one go smoothly in practice.
The costs people underestimate
When teams choose microservices, they usually budget for the fun part (writing services) and not for the enduring part (running them). In an enterprise architecture transformation, the recurring costs dominate:
- Distributed data. You lose cross-service ACID transactions. You now need patterns like the Saga for multi-step consistency, and you must design for eventual consistency and idempotency.
- Network as a failure domain. Every in-process call that becomes a network call can time out, retry, and cascade. You need circuit breakers, bulkheads, and sensible timeouts.
- Observability. Debugging across services requires distributed tracing, correlation IDs, and centralized logging. Without it, you are flying blind.
- Operational overhead. Service discovery, API versioning, deployment pipelines per service, and more on-call surface area.
Here is the kind of consistency handling you inherit the moment you split a transaction across services:
Saga: Place Order
1. OrderService: create order (PENDING)
2. PaymentService: charge card
- on failure -> OrderService: cancel order (compensate)
3. InventoryService: reserve stock
- on failure -> PaymentService: refund (compensate)
OrderService: cancel order (compensate)
4. OrderService: mark order CONFIRMED
That compensation logic is code you write, test, and maintain. In a monolith, the database did it for you with a rollback. This is the real trade, and it is why "it depends" is the honest answer.
A pragmatic sequence I recommend
For most organizations, an effective application modernization strategy follows this order:
- Stabilize and measure. Add tests, logging, and metrics to the legacy system first. You cannot safely change what you cannot observe.
- Modularize the monolith. Establish clean internal boundaries aligned to business capabilities. This is refactoring the monolith, and it is valuable even if you never extract a single service.
- Build the operational foundation. CI/CD, observability, and deployment automation. These are prerequisites for distributed systems and improvements for monoliths.
- Extract selectively. Use the Strangler Fig to peel off the one or two capabilities with the strongest case: a scaling hotspot or a high-churn module owned by a separate team.
- Reassess. Measure whether extraction delivered the expected benefit before extracting the next one. Let evidence, not a roadmap slide, drive the next split.
This sequence de-risks the program. Each step delivers value independently, and you stop at the point where further distribution would cost more than it returns.
If you want a structured assessment of where your systems sit on this spectrum, that is the kind of work our team supports through our IT strategy and engineering capabilities. The right answer also varies by domain, which is why we factor in the regulatory and workload realities of the industries we work in.
The bottom line
Choose the architecture that matches your organizational and operational reality, not the one that signals technical sophistication. A well-modularized monolith is a legitimate, modern endpoint for many systems. Microservices are the right call when independent deployment, independent scaling, or fault isolation are genuine, present needs. In nearly all cases, the safest path through legacy system modernization is incremental: stabilize, modularize, build operational muscle, then extract the few services that clearly earn their keep.
FAQ
Is a monolith outdated or legacy by definition?
No. "Monolith" describes a single deployment unit, not an age or quality level. A clean, modular monolith is a modern, well-supported architecture. Many systems labeled "legacy" are problematic because of poor structure, missing tests, and manual deployment, not because they ship as one unit. Fixing those issues often matters more than splitting the deployable.
How do I decide which service to extract first?
Pick the capability with the strongest concrete case: a scaling hotspot with a distinct load profile, or a high-churn module owned by a team that is blocked by shared deployments. Avoid starting with your most critical, transaction-heavy core. Extract something with clear boundaries and meaningful payoff so you learn the operational patterns before tackling harder splits.
What is the Strangler Fig pattern?
It is an incremental migration approach where you place a routing facade in front of the legacy system, build new functionality or extracted capabilities behind it, and gradually redirect traffic until the old code is unused and can be retired. It avoids big-bang rewrites and lets you roll back one capability at a time.
Do microservices improve performance?
Not inherently. In-process calls become network calls, which add latency and failure modes. Microservices can improve performance for specific workloads by letting you scale a hotspot independently, but a monolith often has lower latency for tightly coupled operations. Treat performance as a per-capability question backed by measurement, not a blanket expectation.
Can we run a hybrid of monolith and microservices?
Yes, and most real systems end up here. A stable, transactional core remains a monolith while a few capabilities with distinct scaling or deployment needs run as services. This hybrid is usually the end state of a sound modernization program rather than a compromise.



