Skip to content
Back to writing

Why a Modular Monolith Is Often the Right Platform for a Small Team

Why a Modular Monolith Is Often the Right Platform for a Small Team architecture illustration

Small teams rarely fail because they did not split their application into enough services. They fail because every new dependency creates another runtime, deployment path, credential set, failure mode, and piece of tribal knowledge to operate.

A modular monolith is often the better default: one deployable application with deliberate internal boundaries around business capabilities. It gives the team a coherent place to build pricing, orders, payments, fulfillment, and integrations without making every internal call a network boundary.

This is not an argument for a large unstructured codebase. The value comes from disciplined modules and simple operations, not from putting everything in one folder.

Separate capability boundaries from deployment boundaries

Teams often assume that a well-defined module must be a separate service. Those are different decisions.

A capability boundary answers questions such as:

  • Which module owns the pricing decision?
  • Which module may initiate a payment authorization?
  • Where does order state transition?
  • Which layer translates an ERP response into a business outcome?

A deployment boundary answers whether those modules need independent scaling, availability, security, release, or team ownership. If the business does not yet need that independence, a network boundary adds cost without creating proportional value.

Within a modular monolith, calls are ordinary function calls, local development is straightforward, and refactoring across modules remains practical. The application can still use explicit interfaces, dependency injection, and adapter boundaries. The absence of a network hop does not mean the absence of architecture.

Make module ownership visible

Each module should own its business rules, use cases, and integrations. Other modules depend on its public contract, not its tables or internal helper functions.

For a commerce platform, a useful shape might be:

orders       -> order lifecycle and idempotent operations
payments     -> authorization, capture, refund, and provider adapters
fulfillment  -> routing and shipment lifecycle
integrations -> vendor translation and durable delivery mechanics

The exact directory layout matters less than the dependency direction. An order workflow should not reach into a payment provider client or mutate fulfillment persistence directly. Those shortcuts are how a monolith becomes coupled enough to make later extraction harder than starting with one process ever was.

Keep operational complexity proportional to the team

Every independent service needs deployment automation, logs, metrics, tracing, secrets, runtime configuration, failure handling, version compatibility, and on-call understanding. Add queues, caches, and separate databases, and the number of possible partial failures rises quickly.

For a small team, operational attention is usually scarcer than compute. A single service with a durable database and carefully chosen background jobs can be more reliable than several services that the team cannot observe or recover confidently.

The production order-sync architecture illustrates the point. Durable ingestion, idempotency, classification, reconciliation, and manual recovery are the reliability features that protect the business. Splitting them into services does not remove those requirements; it adds distributed deployment and coordination concerns to each one.

Extract only for a concrete operating reason

Service extraction is justified when a boundary has a real operating need, such as:

  • Independent scaling for a workload with materially different resource use
  • Isolation of untrusted, high-risk, or regulated behavior
  • A distinct availability requirement that cannot share a release cycle
  • A genuinely independent team that owns the capability end to end
  • A mature contract whose release cadence is constrained by the monolith

Even then, extraction should preserve a business contract rather than exposing internal tables or recreating a distributed version of a tangled module. The modular monolith is not discarded; it provides the clean boundary that makes one extraction feasible.

Optimize for the next engineer

The next engineer should be able to trace a request through one understandable runtime: entry point, business decision, persistence, external call, and recovery path. That is easier when the application has consistent module conventions and explicit dependencies.

Use code structure, tests, logging, and documentation to answer:

  • Which module owns this decision?
  • What external effects can it initiate?
  • Can it be retried safely?
  • Which identifiers connect logs to customer and vendor records?
  • Where is the manual recovery path?

If those answers are hard to find in one application, adding service boundaries will make them harder, not easier.

The real tradeoff

A modular monolith accepts shared deployment and some resource coupling. At a certain scale or organizational shape, those costs become real reasons to extract. But starting distributed incurs those costs immediately, before the team has evidence that it needs the benefits.

The practical default is simple: create capability boundaries now, keep the deployment unit simple, and extract only when a concrete operating constraint makes the new complexity worthwhile.

Discuss on Twitter