intermediate 3 min answer

A team chooses a modular monolith over microservices. What makes the module boundaries actually hold, and what signals indicate it is time to extract a service?

modular-monolithboundariesenforcementextractioncoupling
Show the full answer Hide the answer

Why the boundaries erode

A modular monolith gets most of the design benefit of microservices — clear ownership, explicit interfaces, separable domains — without the network, the operational overhead or the distributed-transaction problem.

Its weakness is that nothing prevents a shortcut. In a distributed system, calling another service's database is impossible; in a monolith it is an import statement. Under deadline pressure, someone will reach across a boundary, the change will pass review because it is small, and the boundary will erode gradually until the monolith is no longer modular.

The determining question is whether violations are prevented mechanically or discouraged culturally, and culture does not survive the third quarter of schedule pressure.

Making the boundaries hold

  • Enforce at build time. Module dependency rules checked by the compiler, the build system, or an architecture test that fails the build on a forbidden import. This is the single decisive mechanism — with it the boundaries hold for years, without it they hold for months.
  • Separate database schemas per module, with access permitted only through the owning module. Enforced by database permissions where possible. Shared tables are how a modular monolith becomes a mud ball, and it is the violation that is hardest to undo.
  • Explicit public interfaces, with everything else genuinely private in the language's terms rather than by naming convention.
  • In-process events for cross-module notification, so modules do not call each other synchronously for things that are not queries — which also keeps the eventual extraction straightforward.
  • No shared mutable state across modules.
  • Module ownership by team, with review required from the owning team for changes to its interface.
  • Regular verification that the dependency graph matches the intended one, since the intended one exists in a document and the actual one exists in the code.

The signals for extraction

Extract when a module has a genuinely different requirement, not because the codebase is large:

  • Different scaling profile. One module needs ten times the capacity of the rest, and scaling the whole deployment to serve it is wasteful. The most common legitimate reason.
  • Different availability requirement, where a module must survive the rest failing, or must not be able to take the rest down.
  • Different deployment cadence, where one module needs to release many times a day and the rest cannot.
  • Different technology genuinely required — a language, a runtime, a specialised library.
  • Team autonomy at a scale where deployment coordination has become the bottleneck, evidenced by measured lead time rather than asserted.
  • Resource isolation: one module's memory or CPU behaviour destabilising the others.

The signals that are not reasons

  • "The codebase is large." Size is managed by modularity, which you already have.
  • "Microservices are best practice."
  • "Teams want independence" without a measured coordination cost.
  • "It will scale better" with no measurement showing the current design does not.

The payoff of having done it properly

A well-bounded module extracts in days rather than months. The interface already exists, the data is already separate, the events already flow. That is the actual argument for the modular monolith: not that services are wrong, but that you can defer the decision until you have evidence, at almost no cost, provided the boundaries were enforced mechanically from the start.