Menu
Theme

Modular Monolith

What a modular monolith is, how its modules keep their boundaries, and when it beats both a classic monolith and microservices. For architects structuring a product that several people or teams will grow for years.

Researched
15 Oct 2026
Reading time
6 min
Sources
17
Review due
15 Oct 2027
TL;DR
  • One deployable, many modules: each module is a business capability with a small public contract and private internals.
  • Boundaries need enforcement: project references, visibility and architecture tests, or modules decay into a big ball of mud.
  • Each module owns its data: its own schema, no shared tables, communication through contracts or events.
  • Monolith first: you learn the right boundaries in-process, where moving them is cheap, and extract services only when needed.
  • Inside a module, use whatever fits: Clean, Hexagonal, Vertical Slice or plain CRUD.
On this page 9 sections
  1. The idea in one minute
  2. When it fits
  3. How it works
  4. Worked example: a configuration tool
  5. Where it is used in practice
  6. Gains and costs
  7. How it goes wrong
  8. Testing a modular monolith
  9. Origins

The idea in one minute

A modular monolith is one deployment unit whose code is split into modules with explicit public contracts and enforced boundaries. Kamil Grzybek puts the first half bluntly: a monolith is simply a system with exactly one deployment unit. The other half is the hard part: each module must be independent, deliver a complete business capability, and hide its internals behind a stable contract.[5]

It sits between two familiar extremes. A classic monolith has one deployable and no internal boundaries, so everything can reach everything. Microservices have hard boundaries, enforced by the network, at the cost of distributed systems. A modular monolith keeps the single deployable and gets its boundaries from code structure and tooling instead.

Modules talk only through small contracts or events; their internals and database schemas stay private. The whole thing still deploys as one process.

When it fits

Fits when… Doesn’t fit when…
A new product whose domain is still being learned Parts need independent release cycles or independent scaling
Several developers or a few teams work on one product Parts need different technology stacks
Modules have similar scaling and security needs Many teams need full deployment autonomy
You want the option to extract services later, without paying for them now The system is small enough that one well-layered codebase is plenty

Martin Fowler’s advice is to start new projects as a monolith, learn where the boundaries are, and pay the “microservice premium” only once the complexity demands it.[10][11] Simon Brown’s version: if you cannot build a well-structured monolith, microservices will not save you.[12]

How it works

Modules are business capabilities

Shopify reorganised its Rails monolith away from the framework’s layout (models, views, controllers) into real-world domains such as orders, shipping, inventory and billing.[1] Grzybek’s definition makes the same point: a module delivers a complete capability, not a technical layer.[5]

Public contract, private internals

Spring Modulith describes a module in three parts: a provided interface (what it exposes, including events), an internal implementation, and a required interface (what it needs from others).[7] In .NET the equivalent is a small Contracts assembly per module, with everything else internal. Grzybek’s reference implementation lets modules reference only each other’s integration-events assemblies.[6]

Each module owns its data

Shopify set the rule that each component should exclusively own its data and counted cross-component associations as violations.[1] Grzybek’s implementation gives every module its own database schema;[6] in EF Core that is one DbContext per module with its own default schema.[16]

Communication through contracts or events

Modules call each other in-process through contract interfaces, or publish events. Spring Modulith records event publications in the same transaction and retries failed listeners, which works as an outbox.[9] Grzybek’s implementation is deliberately stricter: modules communicate only asynchronously, through an outbox and inbox per module.[6]

Boundaries must be enforced

  • Spring Modulith verifies three rules: no cycles between modules, no access to another module’s internals, and only declared dependencies.[8]
  • Shopify built Packwerk, a static checker for dependency and privacy violations that can record existing violations so only new ones fail the build.[3]
  • .NET: project references and internal are the first line of defence; architecture tests with NetArchTest catch the rest.[15]

Worked example: a configuration tool

An illustrative layout for a configuration product with four modules.

src/
  Host/ConfigTool.Api               // composition root; references *.Module and *.Contracts only
  Modules/
    Catalog/        Catalog.Contracts        | Catalog.Module        (schema "catalog")
    Configurations/ Configurations.Contracts | Configurations.Module (schema "config")
    Deployment/     Deployment.Contracts     | Deployment.Module     (schema "deploy")
    Licensing/      Licensing.Contracts      | Licensing.Module      (schema "licensing")
  BuildingBlocks/   // outbox, event bus abstraction; no business code
tests/
  ArchitectureTests/                // module boundary rules
  Catalog.IntegrationTests/ ...     // one suite per module
[Fact]
public void Modules_only_reference_other_modules_through_contracts()
{
    var result = Types.InAssembly(typeof(ConfigurationsModule).Assembly)
        .ShouldNot()
        .HaveDependencyOnAny("Catalog.Module", "Deployment.Module", "Licensing.Module")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Inside each module, choose the structure that fits: a Clean or hexagonal core for the rule-heavy Configurations module, vertical slices for the CRUD-like Catalog. Grzybek’s reference implementation uses Clean Architecture inside every module.[6]

Where it is used in practice

  • Shopify. Work began in 2017; by 2020 there were 37 components and a dense dependency graph with many cycles.[2] The in-house tracing tool was replaced by Packwerk.[3] In 2024 the team removed privacy checks from Packwerk, found that zero violations did not mean a package could run in isolation, and concluded that the tool “is a sharp knife, and it must be wielded with care.”[4]
  • Spring Modulith turns the style into a framework for Spring applications, with verification and module-level tests built in.[7]
  • Kamil Grzybek’s reference implementation shows the full .NET version: schema per module, outbox and inbox, architecture tests, Clean Architecture inside modules.[6]

Gains and costs

What it buys

  • One pipeline, one process. No network hops, simple transactions, easy refactoring across modules.
  • Clear ownership. Errors and chores route to the owning module; Shopify used this to spread upgrade work.
  • Replaceable parts. An isolated module can be rewritten or replaced behind its contract.
  • A path to services. A clean module is the cheapest possible starting point for extraction.
  • Low ceremony at the top. One repository, one build, one deployment.

What it costs

  • Discipline and tooling, forever. Boundaries erode without checks in CI.
  • Deployed and scaled together. One release train, one runtime, one technology stack.
  • Boundary design is hard. Wrong cuts are cheaper to fix than in microservices, but still costly.
  • False confidence. Passing static checks is not the same as being able to run a module alone.

How it goes wrong

  1. Shared tables. Two modules reading and writing the same table quietly undo every other boundary.[1]
  2. Hiding internals but keeping the tangle. Shopify found encapsulation useless while the dependency graph stayed dense and cyclic.[2]
  3. Modules drawn by layer or by name. Boundaries must follow function and capability, not folder names.[4]
  4. Over-prescriptive interface rules that make every call a ceremony.[2]
  5. Static checks as the only proof. Boot and run a module on its own from time to time.[4]
  6. Extracting a coupled module. Turning an entangled module into a service creates a real distributed monolith.

Testing a modular monolith

Level What How
Architecture Boundaries, no cycles, no internals access Spring Modulith verify(), NetArchTest rules[8]
Module One module with its neighbours stubbed Spring’s module tests boot a single module; needing many beans from other modules is itself a coupling signal[17]
Use case Each use case against a real database Grzybek’s implementation mocks only external systems[6]
Contract Each module’s public contract and event shapes Tests owned by the providing module

Origins

  • 2015: Martin Fowler’s “MonolithFirst” and “MicroservicePremium” argue for starting monolithic, with careful modularity at both API and data level.[10][11]
  • 2017 to 2024: Shopify’s componentisation of its core monolith, from component labels to Packwerk and its retrospective.[1][4]
  • 2018 onward: Simon Brown’s talks popularise the term; Kamil Grzybek publishes the primer and .NET reference implementation in 2019.[12][5]
  • 2023: the Prime Video debate brings monoliths back into the mainstream conversation.[13]

Key takeaways

  1. Split by business capability, not by technical layer; a module delivers a complete capability.
  2. Expose a small contract (interfaces, integration events) and keep everything else internal.
  3. Give every module its own data; shared tables silently undo every other boundary.
  4. Verify boundaries in CI and, from time to time, actually run a module alone: static checks alone give false confidence.
  5. Move a module to a service only when it needs independent deployment, scaling or technology.

Sources

  1. Deconstructing the Monolith: designing software that maximizes developer productivity · Shopify Engineering, 2019Primaryshopify.engineering
  2. Under Deconstruction: the state of Shopify's monolith · Shopify Engineering, 2020Primaryshopify.engineering
  3. Enforcing modularity inside a Rails monolith with Packwerk · Shopify Engineering, 2020Primaryshopify.engineering
  4. A Packwerk retrospective · Shopify Ruby team, 2024Primaryrailsatscale.com
  5. Modular Monolith: a primer · Kamil Grzybek, 2019Primarykamilgrzybek.com
  6. modular-monolith-with-ddd · Reference implementation in .NETReferencegithub.com
  7. Spring Modulith: fundamentalsReferencedocs.spring.io
  8. Spring Modulith: verifying module structureReferencedocs.spring.io
  9. Spring Modulith: working with application eventsReferencedocs.spring.io
  10. MonolithFirst · Martin Fowler, 2015Primarymartinfowler.com
  11. MicroservicePremium · Martin Fowler, 2015Primarymartinfowler.com
  12. Modular monolith · Simon BrownPrimarysimonbrown.je
  13. Monoliths are not dinosaurs · Werner Vogels, 2023Primaryallthingsdistributed.com
  14. Prime Video switched from serverless to EC2 and ECS to save costs · InfoQ, 2023Secondaryinfoq.com
  15. NetArchTestReferencegithub.com
  16. Entity types: table schema · EF Core documentationReferencelearn.microsoft.com
  17. Spring Modulith: integration testing application modulesReferencedocs.spring.io
↑ ↓ to moveEnter to openFull-text search by Pagefind