onse
Architecture

Modular Monolith

Design a single deployable application as well-bounded modules with clear ownership, dependencies, and a path to extract services later.

A modular monolith is one deployable unit composed of modules that own their data and expose narrow interfaces. You keep operational simplicity of a monolith while enforcing the boundaries that microservices try—and often fail—to achieve through process isolation alone.

The problem with unstructured monoliths

Many systems start as a single codebase that grows by convenience: any layer may call any repository, shared tables accumulate foreign keys across domains, and “just one more join” becomes the architecture. The result is a ball of mud—hard to reason about, hard to test in isolation, and painful to split when scale or team boundaries demand it.

Microservices are a common reaction. They force boundaries with network calls and separate databases, but they also introduce distributed failure modes, eventual consistency, and operational cost. If the domain boundaries are wrong, you pay that cost twice: once for the distributed system, and again when you reshape services.

Modular monolith

A single process (or single primary deployable) organised into modules with explicit public APIs, owned data, and forbidden cyclic dependencies—without requiring separate services.

Module boundaries

A good module is organised around a business capability, not a technical layer. Examples: Billing, Catalog, Identity, Fulfillment. Each module:

  1. Owns its persistence schema (or clearly owned tables/schemas)
  2. Exposes a public application API to other modules
  3. Hides internal types, repositories, and domain details
  4. Communicates with other modules through that API—not by reaching into their tables

Dependency rules

Enforce a acyclic module graph. Tools and conventions help:

MechanismRole
Project / package boundariesCompile-time prevention of illegal imports
Architecture testsAssert “Billing must not reference Catalog.Internal”
Code reviewsCatch leaked DTOs and shared “utility” domains
Database schemasMake ownership visible at the storage layer

In .NET, this often means one project (or folder with analyzers) per module, plus a thin composition root that wires modules together. In other ecosystems, packages, Bazel targets, or module plugins play the same role.

In-process integration

Modules still need to collaborate. Prefer:

  • Synchronous calls through public interfaces for queries and commands that must complete in one request
  • Domain events published in-process (or via an outbox) when another module should react without tight coupling
// Billing depends on Catalog's public contract—not Catalog's repositories
public sealed class CreateInvoiceHandler
{
    private readonly ICatalogPricing _pricing;
    private readonly IInvoiceStore _store;

    public CreateInvoiceHandler(ICatalogPricing pricing, IInvoiceStore store)
    {
        _pricing = pricing;
        _store = store;
    }

    public async Task<InvoiceId> HandleAsync(CreateInvoice cmd, CancellationToken ct)
    {
        var price = await _pricing.GetUnitPriceAsync(cmd.Sku, ct);
        var invoice = Invoice.Create(cmd.CustomerId, cmd.Sku, price, cmd.Quantity);
        await _store.SaveAsync(invoice, ct);
        return invoice.Id;
    }
}

This is the same dependency direction you would want between services—without the network yet.

When to choose a modular monolith

Choose it when:

  • One team (or a few closely collaborating teams) owns the product
  • Consistency and simple transactions matter more than independent deploy cadence
  • You are still discovering bounded contexts
  • Operational maturity for many services is not yet in place

Defer microservices until you have a concrete driver: independent scale of a hotspot, regulatory isolation, or team autonomy that cannot be met with module ownership alone. Extraction then becomes moving a module behind a remote API and migrating its data—not inventing boundaries from a mudball.

Relation to other styles

Modular monoliths pair well with hexagonal architecture inside each module: ports and adapters keep domain logic free of infrastructure. When cross-module reactions grow asynchronous, event-driven architecture patterns apply in-process first, then across services if you extract.

Scale concerns such as scaling writes and database sharding remain relevant inside a monolith: modules that own write-heavy data can still partition storage without splitting the deployable.

Anti-patterns

  • “Shared Kernel” that grows forever — a dumping ground for entities everyone mutates
  • Module façades that leak ORM entities — the public API becomes the database
  • Circular module references — usually a missing third concept or wrong ownership
  • Premature extraction — splitting for resume-driven architecture before boundaries stabilize

Related articles

On this page