skip to content

What is a 'modular monolith,' and how does it try to capture the team-autonomy benefits of microservices while avoiding the distributed-system costs of splitting services over the network?

level: middleimportance: must knowfreq 75%

answer

  1. package-private/module API boundary
  2. in-process calls stay ACID
  3. extraction seam for later
  4. Spring Modulith / ArchUnit enforcement
  5. boundary erosion under deadline pressure

basics

~20 s

A modular monolith is one deployable app internally split into strict modules with clear boundaries, so teams work independently like in microservices, but everything still runs and deploys as one process, so there's no network overhead or distributed failure modes.

solid answer

~50 s

A modular monolith keeps the single-process, single-deployment, often single-database shape of a monolith, but enforces module boundaries inside the codebase as strictly as if they were separate services: each module owns its own package/namespace, exposes a narrow public API, and is forbidden — usually by build-time tooling like ArchUnit or Spring Modulith — from reaching into another module's internals or its private tables. Calls between modules stay in-process function calls, so they're synchronous, type-safe, and can share a single ACID transaction; there's no network, no serialization, no partial failure. You get much of the team-ownership and code-organization benefit of microservices without paying the distributed-system tax, because a bad call between modules is a compile error, not a 3am page. The main things you give up are independent deployability and independent runtime scaling of a single module, since the whole application still ships and scales as one unit.

go deeper

for a junior

Should be able to describe that a modular monolith is one deployable app with internal boundaries, and give one reason teams pick it over both a plain monolith and microservices.

for a middle

Should name at least one concrete enforcement mechanism (build-time checks, package visibility) and explain that in-process calls stay synchronous/transactional, unlike calls between real microservices.

for a senior

Should discuss the data-ownership angle (shared DB risk), boundary erosion under pressure, and how the pattern sets up a low-cost extraction path to microservices later.

for a principal

Should be able to weigh when a modular monolith is the wrong call — e.g., when a module needs an independent scaling profile — and describe how to evolve the architecture incrementally rather than as a big-bang rewrite.

## What a modular monolith is A modular monolith is an **architectural middle ground**: the deployment topology of a classic monolith — one build artifact, one running process (or horizontally scaled copies of that same process), typically one database — combined with the internal discipline of microservices, where the codebase is partitioned into modules with explicit, enforced boundaries. Concretely, each module (say, billing, inventory, notifications): - lives in its own package or Gradle/Maven module - exposes only a small public interface that other modules are allowed to call - keeps its internal classes, and critically its own database tables, private Tooling enforces this at build time: in the JVM world, **Spring Modulith** or **ArchUnit** rules fail the build if module `inventory` imports an internal class from `billing` instead of going through billing's published API; in other ecosystems, module linters or internal/package-private visibility modifiers do the same job. ## Why the pattern exists The reason this pattern exists is that the two headline benefits people reach for microservices for — **team autonomy** and **enforced separation of concerns** — don't actually require crossing a process boundary. A network call between two microservices and a compiler-enforced call between two in-process modules both prevent module A from reaching into module B's internals; the difference is that the in-process call is synchronous, type-checked, participates in the same transaction, and fails in exactly the ways a normal function call fails (an exception), not in the many new ways a network call can fail (timeout, partial success, duplicate delivery, version skew between independently-deployed callers). A modular monolith captures the organizational discipline without incurring the operational cost of distribution. ## The trade-offs The trade-offs run in both directions. **What you keep, relative to an unstructured monolith:** - clear ownership boundaries that make onboarding and code review tractable - the option to extract any module into a real microservice later with comparatively little pain (since its public API is already the seam) - dramatically simpler local development — one process to run, one debugger to attach **What you give up, relative to real microservices:** - you cannot deploy the billing module's bug fix without redeploying and re-testing the whole application - you cannot scale inventory independently if it's the CPU-hungry module — the whole process scales as a unit - because modules commonly still share one physical database, a careless migration or an accidentally-shared table can silently reintroduce coupling that the code-level boundary was supposed to prevent, unless the team also enforces schema-level separation ## Failure modes The failure modes of a modular monolith in production are mostly failures of discipline rather than of the pattern itself. 1. The most common is **"boundary erosion"**: under deadline pressure, an engineer adds a direct database query or a direct class reference across a module boundary because it's faster than going through the public API, and without automated enforcement that erosion compounds silently until the "modular" monolith is just a monolith with folders. 2. **A second failure mode** is treating the module split as a substitute for good domain design: mechanically drawing module lines around existing classes without a real bounded-context analysis just produces internal spaghetti with extra ceremony. 3. **A third is scaling limits** — if one module truly needs orders of magnitude more compute or a very different scaling profile, the shared-process constraint becomes a real ceiling that only extraction into a separate service can lift. ## Where it shows up A concrete real-world reference point is **Shopify's** well-documented move to a modular monolith for its core Ruby application: after early experiments extracting services, Shopify found that for its scale and team structure at the time, most services still needed to talk to the core commerce logic constantly, so they invested instead in enforcing module boundaries (via tooling akin to **Packwerk**) inside the Rails monolith rather than paying network overhead for calls that were conceptually still "the same request." The **Spring Modulith** project formalizes the same idea for the Java/Kotlin ecosystem, providing build-time verification of module boundaries plus optional event-based communication between modules, explicitly positioning itself as a stepping stone that keeps the option to extract a module into a real microservice open without forcing that decision before the domain boundaries are proven stable.

  • How would you enforce module boundaries so they don't erode over time?
    Add an automated build-time check — ArchUnit rules, Spring Modulith's verification, or a module-boundary linter — that fails CI if code outside a module's public package imports its internal classes. Pair this with a code-review norm of rejecting any cross-module call that bypasses the published API, and periodically audit for direct cross-module database access.
  • If a modular monolith is disciplined about boundaries, how hard is it later to extract a module into a real microservice?
    It's meaningfully easier than extracting from an undisciplined monolith, because the module's public API is already the intended seam — you replace in-process calls with network calls implementing the same interface. It's still real work: you now need to handle network failures, decide on data ownership (splitting the shared database), and manage independent deployment, but the domain boundary itself doesn't need to be rediscovered.
  • Can a modular monolith share one database across all modules safely?
    It can, but only if the discipline extends to data — each module should own its own tables or schema and other modules should go through its API rather than querying its tables directly. A shared database with unrestricted cross-module SQL access defeats the boundary at the data layer even if the code-level API looks clean.

A modular monolith is like an open-plan office with clearly marked team areas and a rule that you must email a request rather than walk over and grab something off someone else's desk — you get the separation of responsibilities without needing separate buildings connected by courier.

saying these in an interview costs you the question

  • Treats a modular monolith as just 'a monolith with folders,' with no enforcement mechanism
  • Doesn't know that modules can still share a database, and that this is a common way boundaries erode
  • Assumes a modular monolith gives independent deployability of individual modules
  • Can't name any concrete tool or mechanism (ArchUnit, Spring Modulith, package-private visibility) used to enforce the boundary
  • Thinks the pattern only matters as a stepping stone and has no standalone value

context