Book a call

Modular Monolith: What It Is and When It Beats Microservices

Most teams that move to microservices are really after clear boundaries. A modular monolith gives you those boundaries first and keeps the option to split later.

Isometric illustration: one frosted glass block divided into sealed compartments, each holding a grey shape, with one compartment slid out holding a black pyramid, on a yellow background

One deployable, many modules with hard edges

A modular monolith is a single application, built and deployed as one unit, that is divided inside into modules along business lines: orders, billing, catalog, accounts. Each module owns its data and its rules, and other modules can reach it only through a small public API. You get one codebase, one deploy and one database server, with boundaries as strict as the ones between microservices.

The architecture fixes the problem most teams actually have. A tangled monolith hurts because any change can break anything, not because it runs as one process. Splitting that code into services keeps the tangle and adds a network to it. Drawing and enforcing module boundaries removes the tangle and leaves the network out until you have a reason to pay for it.

Modular monolith vs microservices

Both styles split a system by business capability. They differ in where the boundary is enforced and what it costs to cross it.

  • Deployment. A modular monolith ships as one artifact. Microservices ship independently, so each needs its own pipeline, versioning and rollback.
  • Calls between parts. In a modular monolith a call to another module is a function call: nanoseconds, no timeouts, one stack trace. Between microservices it is a network call that can be slow, fail halfway or arrive twice.
  • Data. Modules keep their tables in separate schemas of one database, so a change that touches two modules can still run in one transaction. Services own separate databases, and cross-service changes need sagas, outbox tables or accepted inconsistency.
  • Scaling. A modular monolith scales as a whole: you run more copies of the entire app. Microservices let you scale only the part under load.
  • Team independence. Modules give teams separate code ownership but a shared release train. Services give teams separate releases, at the price of contracts and coordination across the network.
  • Operations. One app means one set of logs, metrics and alerts. Twenty services mean distributed tracing, a service mesh or gateway, and on-call for each.

Microservices win on independent scaling and independent releases. A modular monolith wins on everything that touches cost and speed of change, until one of those two needs becomes real.

What makes a module a module

Folders named after business areas are not modules. A module boundary exists only if the code cannot cross it by accident. Four rules make that true:

  1. Each module owns its data. Billing tables live in a billing schema, and only billing code reads or writes them. No other module joins against them, even though it could.
  2. Modules talk through a public API. Each module exposes a small set of functions, commands or events. Everything else is internal. Orders asks billing to create an invoice; it does not insert a row into the invoices table.
  3. Boundaries follow business capabilities. "Orders" is a module. "Repositories" and "controllers" are not. A change to how refunds work should touch one module, not a slice through all of them.
  4. The build enforces the rules. A test in CI fails when one module imports another module's internals. Without that check, deadline pressure erodes the boundaries in months.

The fourth rule is the one teams skip, and it is the cheapest. Every major stack has a tool for it: Spring Modulith and ArchUnit for Java, NetArchTest for .NET, Packwerk for Ruby on Rails, import-linter for Python, and Nx module boundaries or dependency-cruiser for TypeScript.

Hand-drawn example: one app with Orders, Billing and Catalog modules, each with a public API and its own schema; Billing may call the Orders public API but not Orders internals, and an import-linter check in CI reports the broken contract shop.billing.invoices to shop.orders.internal.repository
The fourth rule in practice: a forbidden import fails the build, so the boundary survives deadline pressure.

When a modular monolith is the right choice

Start with a modular monolith when most of these are true:

  • One to five teams work on the product, and they can release together without blocking each other.
  • The domain is still moving. You are not yet sure where the boundaries belong, and moving a boundary inside one codebase is a refactor, while moving it between services is a migration.
  • No single part of the system has a load profile different enough to need its own scaling.
  • You have no platform team, or it is small. Running many services well needs CI/CD per service, tracing, contract tests and someone who owns all of that.

Large companies run this way on purpose. Shopify's core commerce application is a Ruby on Rails monolith of millions of lines, split into components with ownership enforced by its own open-source tool, Packwerk. In 2023 Amazon Prime Video's team for audio and video quality monitoring moved that service from distributed serverless components into a single process and reported that its infrastructure cost fell by more than 90%.

When to split a module out into a service

A module earns its own deployment when it has a need the rest of the app does not share. The signals worth acting on:

  • Different scaling. Image processing, search indexing or a public API under spiky traffic needs ten times the capacity of the rest of the app for part of the day.
  • Different release pace or owner. One team ships the module daily, the rest of the product ships weekly, and the shared release train is a measurable delay.
  • Different isolation needs. Card data under PCI DSS or patient data under HIPAA is easier to audit when it lives in a separate service with a separate access boundary.
  • Different runtime. The module needs GPUs, a Python ML stack or a language the main app does not use.

Team size alone, or a wish to look modern, is not on the list. When a real signal appears, a well-built module is cheap to extract: its public API becomes an HTTP or messaging contract, its schema moves to its own database, and nothing else in the app changed shape. That is the payoff of a modular monolith: the expensive decision waits until you have evidence for it.

Where modular monoliths go wrong

  • The shared module. A "common" or "core" module that every other module depends on grows until it holds half the business logic. Keep shared code to technical utilities; business rules belong to one owner.
  • Joins across schemas. One report query that joins orders and billing tables couples them forever. Build reports from events or from a read model instead.
  • Events for everything. Turning every call between modules into an asynchronous event brings back the hardest part of microservices, eventual consistency, without the scaling benefit. Use direct calls by default and events where a module truly should not wait.
  • No enforcement. Boundaries described in a wiki page but not checked in CI last until the first deadline.

How to get from a tangled monolith to modules

  1. Map the domain. List the business capabilities and which tables each one writes. Tables written by two capabilities mark the boundaries that need the most work.
  2. Pick module boundaries and name owners. One team owns each module, even if one team owns several.
  3. Add the boundary check in report mode. Run the architecture test in CI and record the violations without failing the build. The count is your baseline.
  4. Fix violations one module at a time. Replace direct table access and internal imports with calls to the public API, then switch that module's check to fail the build.
  5. Split the database by schema. Move each module's tables into its own schema and remove cross-schema joins.

Most teams can do this alongside feature work, module by module, without a freeze. If the boundaries hold for a quarter and a module still shows one of the signals above, extract it. We describe that decision in more detail in our monolith to microservices decision framework, and the practices that keep extracted services healthy in our microservices best practices guide.

We help teams draw module boundaries, add the checks that keep them, and decide which modules, if any, should become services. See software architecture consulting.

Read next

planning a new project
We’ll help you choose the right discovery depth and map a realistic starting plan.
Isometric blue glass blueprint plate with glass blocks rising from it and one yellow block

Recent blog posts

Read More Articles
Isometric illustration: one frosted glass block divided into sealed compartments, each holding a grey shape, with one compartment slid out holding a black pyramid, on a yellow background
Modular Monolith: What It Is and When It Beats Microservices
View insights

Most teams that move to microservices are really after clear boundaries. A modular monolith gives you those boundaries first and keeps the option to split later.