Stackbook Logo
deploymentestablished · low operational burden

Modular Monolith

Also known as: modulith, well-structured-monolith

Intent

Build a single deployable application composed of strongly isolated modules with explicit interfaces.

Problem

Classic monoliths decay into tangled code where everything calls everything, while microservices add heavy distributed-systems costs a team may not need. Teams want clear boundaries without network overhead.

Forces

  • •Boundaries are valuable even when deployment is unified
  • •Distributed systems introduce latency, failure modes, and operational cost
  • •Modules should be extractable later if needed
  • •Discipline is needed since the compiler alone will not enforce boundaries

Solution

Overview

A modular monolith keeps one deployable but treats modules like internal services: each owns its logic and data, and talks to others only through a published contract. It captures many benefits of microservices, such as clear ownership and low coupling, without network calls, distributed transactions, or per-service pipelines.

How it works

Modules map to business capabilities such as Catalog, Ordering, and Billing. Each has a public facade and private internals.

app/
  ordering/   api/ (public)  internal/ (private)  schema: ordering
  billing/    api/ (public)  internal/ (private)  schema: billing
  catalog/    api/ (public)  internal/ (private)  schema: catalog
Rule: ordering may import billing.api, never billing.internal

Cross-module reactions use in-process events or the public API. Architecture tests, for example with ArchUnit or dependency-cruiser, fail the build on violations. Because calls are in-process, a failed call is an exception, not a timeout, and a single database transaction can still cover a workflow if needed, though keeping modules decoupled is better.

Pitfalls

  • Joining across module tables in SQL, which couples schemas.
  • Skipping enforcement and trusting convention.
  • Sharing domain entities between modules.
  • Splitting into services before measuring a real need.
  • Creating a catch-all common module.

Key takeaways

  • Strong boundaries do not require network boundaries.
  • Own data per module and enforce rules in the build.
  • Design modules so extraction is possible, then extract only when scaling or team structure demands it.

✓ When to Use

  • •Small to mid-size teams building a complex product
  • •Early stage where domain boundaries are still evolving
  • •Teams wanting autonomy without microservice operations

✗ When Not to Use

  • •Parts that need independent scaling or different runtimes
  • •Many teams that must release on independent cadences
  • •Hard fault-isolation requirements between components

Pros

  • +Simple deployment and debugging
  • +In-process calls and local transactions
  • +Clean path to extraction

Cons

  • −Boundaries rely on discipline and tooling
  • −Single scaling and release unit
  • −One module bug or leak can affect the whole process

Cost Profile

Infrastructure

Low — single deployment

Operational

Low — one pipeline and runtime

Cognitive

Medium — boundary design and enforcement

Failure Modes

  • Modules reaching into each other's tables, creating hidden coupling

  • Shared utility module that grows into a dumping ground

  • Boundaries never enforced, reverting to a big ball of mud

Real-World Examples

Alternatives

    Related Patterns

      Competency Domains

      economics evolutionrequirements constraints