Skip to content

Microservices vs modular monolith

Both styles divide a system into modules with clear boundaries. The difference is whether those modules are deployed and run separately (microservices) or together as one application (modular monolith).

Modular monolith Microservices
Deployment One unit Many independent units
Module communication In-process function calls Network calls and messages
Data One database, separate schemas per module A database per service
Transactions Ordinary database transactions Sagas and eventual consistency
Scaling Whole application scales together Each service scales on its own
Operational cost Low High
Team independence Moderate: shared release train High: independent releases
Cost of moving a boundary Low: a refactor High: a migration
  • The team is small or medium-sized (roughly one to a few teams).
  • The domain is still being discovered and boundaries may move.
  • You don’t yet have mature deployment automation and observability.
  • Strong consistency across modules matters, for example in financial postings.
  • Many teams are slowed down by a shared release process.
  • Specific parts need independent scaling, availability or technology.
  • Boundaries are well understood and stable.
  • You can afford the platform: CI/CD per service, tracing, service discovery and on-call.

Start with a modular monolith unless you have a concrete reason not to. Enforce module boundaries strictly (no reaching into another module’s tables or internals), and a module can later be extracted into a service when real pressure appears. Going the other way, merging badly split services back together, is far more painful.

Worked decision: extracting a payments module

Section titled “Worked decision: extracting a payments module”

An illustrative scenario showing how the verdict above is applied in practice.

A lending platform runs as a modular monolith with five modules: applications, underwriting, loan servicing, payments and notifications. Two years in, the payments module is under pressure:

  • Payment processor integrations change monthly, but every change waits for the whole platform’s fortnightly release.
  • Card and bank data handled by the payments module puts the entire application inside the scope of payment-security audits.
  • Month-end collection runs create load spikes ten times normal traffic, all in payments.
  • A dedicated payments team now exists, separate from the lending teams.
Signal Present? Points toward
Independent release cadence needed Yes Extract
Compliance or security scope to isolate Yes Extract
Distinct scaling profile Yes Extract
Dedicated owning team Yes Extract
Boundary stable for a year or more Yes Extract
Strong transactional coupling to other modules Partly: a payment updates the loan balance Stay, unless the coupling can be made asynchronous

Five strong signals and one manageable obstacle. The coupling is handled by making the loan balance update asynchronous: payments publishes a PaymentSettled event, and loan servicing applies it idempotently.

ADR-014: Extract payments into a separate service
Status: Accepted
Context: Payments needs independent releases, its own scaling and a reduced
audit scope. Its module boundary has been stable for 18 months and
a dedicated team owns it.
Decision: Extract the payments module into a separately deployed service
with its own database. Loan servicing consumes PaymentSettled
events instead of sharing a transaction with payments.
The other four modules remain in the monolith.
Consequences:
+ Payments releases independently; audit scope shrinks to one service.
+ Collection spikes scale payments alone.
- Loan balances become eventually consistent (seconds, not instant).
- Requires event delivery guarantees, idempotent consumers and
reconciliation between payments and loan ledgers.
Alternatives considered:
- Stay in the monolith with stricter module rules: doesn't solve audit
scope or release cadence.
- Split the whole platform into services: no other module shows the
signals above.
Revisit if:
- Reconciliation finds unexplained mismatches between ledgers.
- Most payments changes still need coordinated monolith releases.
- Event lag regularly exceeds what customer service can explain.

The decision extracts one module for specific, observable reasons, and leaves the rest of the monolith alone. Each “revisit” condition is something the team can measure in production, so the decision can be checked rather than argued about.

Written and edited by Abhinav Taduka, enterprise software architect.