Monolith vs Microservices: A Reality Check

September 18, 2024

Microservices solve a coordination problem between teams and charge for it in distributed systems complexity. The decision is therefore about organizational scale, not about technical merit, and evaluating it on technical merit produces the wrong answer in both directions.

The framing that leads somewhere: has team size and structure reached the point where independent deployment and enforced domain isolation are worth paying operational cost for. Large-scale service architectures exist because hundreds of engineers in one codebase create coordination problems that architectural separation relieves. Organizations well below that scale are buying the cost without the problem.

Counterexamples at the other end are equally instructive. Shopify operates one of the largest Ruby codebases in production as a modular monolith, through peak retail traffic. Stack Overflow serves very high traffic from a small server footprint. Monolithic architecture is not a scaling ceiling. The complexity of distribution starts paying for itself at a particular organizational size, and not before.

What the operational cost consists of

The bill comes due in categories that do not appear in architecture diagrams.

  • Distributed tracing and correlation. A single user action spans multiple services. Without correlated tracing, diagnosing a failure means reconstructing a timeline by hand across several log stores.
  • Partial failure handling. Every inter-service call can time out, return a partial result, or succeed after the caller gave up. Each call site needs a defined behavior for each case. In a monolith these are method calls that either return or throw.
  • Data consistency without transactions. Operations spanning services cannot use a database transaction. The replacements — sagas, compensating actions, eventual consistency with reconciliation — are more code and a new class of defect.
  • Deployment and version compatibility. Services deploy independently, which means every version of every service must interoperate with every version of its dependents that could be live simultaneously.
  • Environment provisioning. A developer needs a working system to test against. In a monolith that is one process.

None of these are unsolvable. All of them are recurring engineering cost that produces no customer-visible capability.

The condition usually mistaken for a monolith problem

When a team reports that the monolith is slowing them down, the constraint is generally the absence of internal boundaries rather than the single deployment unit.

The observable state: everything depends on everything, no module owns its data, a change in one area produces failures in an unrelated one, and no one can predict the blast radius of a change. Decomposing that system into services distributes the same dependency graph across a network, converting method calls into network calls and compile-time errors into runtime ones.

The intervention that addresses the actual constraint is modular structure inside the single deployment unit:

  • Boundaries drawn around business capabilities, with a module owning its own data and exposing an explicit interface
  • No cross-module database access — a module reads another module's data through its interface, not its tables
  • Dependencies between modules declared and enforced in the build, so violations fail the build rather than accumulating
  • Each module independently testable

This produces the isolation benefit at compile time and in a single process. If organizational scale later requires independent deployment, the seams exist and extraction is a mechanical exercise. Building the distributed infrastructure first, in order to obtain the seams, reverses the sequence and pays the operational cost during the period when it delivers nothing.

Conditions that justify separate services

Four cases hold up, and each is specific enough to test against.

  • Divergent scaling profiles. One component requires substantially different resources than the rest, and scaling the whole unit to serve it is materially wasteful. Compute-heavy processing next to a lightweight web tier is the common instance.
  • Isolation required by compliance. A regulatory requirement mandating that a capability run in a separate environment with separate access control.
  • Deployment cadence conflict between teams. A team deploying many times a day blocked by a team deploying monthly, with the coordination cost measurable in delayed releases.
  • Independent availability requirements. A capability that must remain available while the rest of the system is down, or vice versa.

The test before committing

State which of the four conditions applies, with the evidence for it.

"We want to use different technology stacks" and "current practice favors this architecture" are preferences, and they do not cover the operational cost. "The payments team ships several times daily, the analytics team ships monthly, and each blocks the other" is a coordination problem with a measurable cost, and independent deployment is a direct response to it. The difference between those two answers is the entire decision.


Working through a problem like this?

Describe the system and where it's stuck. I'll tell you what the work actually involves.

Get in touch