Custom Software

Microservices vs Modular Monolith: Choosing the Right Distributed Architecture

Microservices vs Modular Monolith: Choosing the Right Distributed Architecture

Choose a modular monolith when one team owns the domain and transactions must stay simple; choose microservices when independent deployment, independent scaling or independent ownership across several teams is the binding constraint. Most systems should begin as a well-modularised monolith and extract services only when a concrete pressure justifies the operational cost.

The real trade-off

The distinction that matters is not size, it is coupling and deployment independence. A monolith is a single deployable unit; a microservice architecture is a set of independently deployable units that communicate over a network. Everything else, including how big each service is, follows from that.

Distribution converts a function call into a network request, which means it converts a deterministic local operation into one that can time out, partially succeed, be retried, be reordered or be observed twice. That is the cost. The benefit is that a change to one service can ship without coordinating a release with every other team.

So the question is not whether microservices are modern. It is whether your organisation currently has a coordination problem that only independent deployment can solve.

When microservices pay for themselves

Microservices pay when several teams need to ship at different cadences against the same domain, when one component has radically different scaling characteristics from the rest, or when a component has a distinct availability or compliance profile.

They also pay when you need per-component technology choices for genuine reasons, or when the blast radius of a failure must be structurally limited rather than merely monitored.

None of these are about code quality. They are about organisational structure and runtime characteristics, which is why architecture decisions cannot be made from a style guide.

  • Multiple teams contending on one release train.
  • One component needing an order-of-magnitude different scaling profile.
  • Distinct compliance, residency or isolation requirements for one domain.
  • Independent failure isolation as a hard requirement rather than an aspiration.

What a good modular monolith looks like

A modular monolith is not a big ball of mud with a nicer name. It has explicit modules with published interfaces, no cross-module access to internal data structures, and a dependency direction that is enforced mechanically rather than by convention.

The enforcement matters. Module boundaries that exist only in a diagram decay within two quarters. Boundaries enforced by the build, such as separate packages with restricted visibility, an architecture test in the pipeline, or a module dependency linter, survive.

The payoff is substantial: a single transaction can still span modules, local refactoring is possible, debugging uses a single stack trace, and deployment remains one artefact. When a module later needs to become a service, it already has a clean interface to extract along.

Operational and design characteristics
Dimension Modular monolith Microservices
Transaction consistencyLocal ACID transactionsSagas, eventual consistency
Deployment unitOne artefactMany, independently versioned
Failure modeWhole applicationIsolated per service
Local debuggingSingle processDistributed tracing required
Team scalingOne shared release trainIndependent per team
Infrastructure costLowSubstantially higher
Best fitOne or two teams, evolving domainMany teams, stable boundaries

The migration path from monolith to services

Migration should be driven by pain, not by ambition. The first candidate is usually the component with the most extreme scaling profile, because the win is immediate and measurable. The second is the module with the highest change frequency and the noisiest deployment, because that is where release coordination costs the most.

Extract along existing module seams, put a stable interface in front of the extracted component, and keep the monolith as the caller until the new service is proven. Use a strangler pattern so the old path remains available until traffic has genuinely moved.

Critically, do not decompose the database first. Sharing a database between services recreates the coupling you were trying to remove while adding network failure. The service owns its data, and other services read it through the service interface or through explicit, versioned events.

  • Extract the most scaling-constrained module first.
  • Introduce the interface before moving the implementation.
  • Use a strangler pattern so rollback stays cheap.
  • Give each service ownership of its own data store.

Frequently asked questions

Are microservices always more scalable than a monolith?

No. A well-tuned monolith handles very high throughput and is simpler to operate. Microservices allow selective scaling of the hot component and independent failure isolation, which is a different benefit from raw scale.

How many services should a system have?

As few as the ownership and scaling constraints require. A useful heuristic is one service per bounded context per owning team. Service counts driven by technical layers rather than domains produce the worst outcomes.

Can a modular monolith be deployed on Kubernetes?

Yes, and it usually should be. Container orchestration gives you reproducible environments and horizontal scaling without requiring you to split the application into separate deployable services.

References

Related engineering guides

Engineer your next strategic platform

Estimate your project investment and delivery timeline instantly, or schedule an architectural discovery call with our engineering leadership.

Launch Cost Estimator Speak with an Architect