# Application Modernization: Modular Monolith or Microservices First?

For most application modernization projects, the right first step is a modular monolith. That means one deployable application with clean, enforced boundaries between its parts. Split into microservices only after those boundaries have held up in production and you have a specific reason for one part to deploy, scale or fail on its own.

If you are a CTO with an ageing system that nobody wants to touch, you have probably heard two pitches. One vendor wants to carve it into twenty services. Your own team wants to rebuild it on a newer stack and keep it as one app. You need to know which path lowers your risk, not which one sounds more modern.

## Why does this choice matter so much in a modernization?

The choice decides where your complexity lives. A monolith keeps it inside the code. Microservices move it onto the network, the deployment pipeline and the on-call rota. Four factors decide whether your organisation can carry that weight.

### Team size

Services pay off when each one has a team that owns it from code to production. If you have eight engineers and propose fifteen services, everyone touches everything. You get the coordination cost of a distributed system without the independence that justifies it.

A simple test: can you name a team, not a person, for every service on the diagram?

### Deployment overhead

Every service needs its own build pipeline, versioning, configuration, secrets, health checks and rollback plan. When two services talk to each other, any change to that contract has to stay backward compatible until both sides are updated. In a monolith, the compiler and one test suite catch most of that for free.

### Data ownership

This is where most splits go wrong. Ageing systems usually sit on one shared database, with tables joined across every feature. A service that still reads another service's tables is not independent. It just moved the coupling somewhere harder to see.

Real separation means replacing joins with API calls or events, and replacing single database transactions with eventual consistency. A report that ran as one query now needs a data pipeline. That work is often larger than the code split itself.

### Observability

In a monolith, one stack trace tells you what broke. Across services you need distributed tracing (OpenTelemetry is the common choice), correlation IDs on every request, centralised logs and per-service dashboards. Without them, a slow checkout turns into an hour of guessing which hop is at fault.

## What does it cost to split too early?

The usual result is a distributed monolith: services that must be deployed together because they share data or call each other in lockstep. Releases get slower than they were before the project started. Incidents take longer to diagnose. Data drifts out of sync, so an order shows as paid while the invoice never gets created.

Meanwhile, months pass before the business sees anything new, because the team is busy building plumbing. Reversing a bad split is also harder than splitting a clean monolith later.

The worst outcome is a system with all the costs of microservices and none of the independence.

## Who does not need to make this choice at all?

If your system is small, stable and rarely changed, and the real pain is an unsupported runtime or missing security patches, you do not need a new architecture. Upgrade the framework, add tests around the main workflows and move to managed hosting. A capable in-house team can usually handle that without outside help.

## What does a modular monolith look like in practice?

You organise the code into modules by business capability, such as billing, orders and accounts. Each module owns its own tables. Other modules call it through a published interface and never reach into its data directly.

The boundaries are enforced by tooling, not good intentions. ArchUnit does this for Java, Nx module rules or dependency-cruiser for TypeScript, Packwerk for Ruby and import-linter for Python. A pull request that crosses a boundary fails the build.

The order we recommend:

1.  **Map the current dependencies.** Find out which features read and write which tables, and which code calls which.
    
2.  **Put tests around the money paths.** Sign-up, payment, the core transaction. You cannot refactor safely without them.
    
3.  **Draw module boundaries and enforce them in CI.** Start with the boundaries the business already recognises.
    
4.  **Separate the data per module.** One schema per module, no cross-module joins, shared reads go through the owning module.
    
5.  **Run it in production for several release cycles.** Watch where changes still leak across modules.
    
6.  **Extract a service only where a rule below says so.**
    

### When should a module become a service?

*   **If one module needs to scale very differently**, such as file processing or report generation, extract it.
    
*   **If a module has its own team whose releases are blocked by others**, extract it.
    
*   **If a module needs strict isolation for compliance or security**, such as payments or health records, extract it.
    
*   **If the only reason is that microservices feel more modern or easier to hire for**, keep it as a module.
    

The trade-off is real. A modular monolith still shares one runtime, so a memory leak in one module can take down the whole app, and everything ships in one deploy. For most teams that is a fair price for the simplicity.

## What should you require before a team proposes microservices?

Whether the proposal comes from your own engineers or a vendor, ask for these before you approve a split. Each item has a reason behind it.

*   **A dependency map of the current system.** Without it, the proposed boundaries are guesses.
    
*   **A named owning team for every service.** Services without owners decay quickly.
    
*   **A data plan per service.** Which tables it owns, how others get that data and how consistency is handled. A shared database means you are building a distributed monolith.
    
*   **Evidence that the boundary already held as a module.** If it leaks inside one codebase, it will leak worse across a network.
    
*   **Tracing, central logging and alerting in place before the first extraction.** You cannot fix what you cannot see.
    
*   **A shown ability to deploy and roll back one service alone.** If they must ship together, they are not separate.
    
*   **A business reason for each split.** Scale, release speed or isolation, stated plainly.
    
*   **A strangler plan.** Traffic moves to the new service gradually, with the old path kept as a fallback until it is proven.
    

If a proposal cannot answer these, ask for the modular monolith first. The boundaries you build there are exactly what a later split will need.

Geminate Solutions has shipped 50+ products, including an exam platform handling 10M+ requests per minute in production, and the lesson holds at every size: clean boundaries come before separate deployables.

For the full process, from audit to cutover, read our [legacy system modernization guide](https://geminatesolutions.com/blog/legacy-system-modernization). If someone has already proposed a split, you can ask Geminate Solutions for a free review of that plan, with a reply within 24 hours.
