Engineering

Modular Monoliths

One deployable unit, many disciplined boundaries

Lead Summary

A modular monolith is a software architecture style in which the application is deployed as a single unit—one build artifact, one deployment pipeline—while internally organized into modules with strict, enforced boundaries. It sits between the traditional tightly-coupled monolith and the distributed microservices architecture: it retains the operational simplicity of a single process while providing the ownership clarity and parallel development benefits usually associated with distributed systems.

Since 2024, the modular monolith has moved from a pragmatic compromise to an actively recommended default. 42% of organizations that adopted microservices have begun consolidating services back into larger deployable units, and industry consensus now treats microservices as an optional destination earned by demonstrated need rather than a starting-point default. Shopify operates the most-cited production example: a 2.8-million-line Rails monolith that handled 173 billion requests on Black Friday 2024, peaking at 284 million requests per minute, using modular boundaries as both an organizational and a safety mechanism.

Definition & Scope

The term "modular monolith" names a combination of two architectural properties:

  1. Single deployment unit. The entire application is packaged and released together. There is no separate deployment pipeline per module, no container orchestration per service, no inter-service network to configure. This property is what makes it a monolith.

  2. Logical module boundaries. The application's internal code is organized into modules, each corresponding to a distinct business capability or domain. Modules may not access each other's internals; they communicate only through declared public APIs. This property is what makes it modular.

Modules enforce boundaries through code structure, namespace organization, and interfaces—not through physical network separation. Communication between modules occurs in-memory through method calls or internal event buses, which is fundamentally faster than inter-service HTTP calls. Microservices, by contrast, elevate every logical boundary to a physical network boundary—a trade-off modular monoliths explicitly decline to make.

A modular monolith is not merely a well-organized single-project application. The distinction is that module boundaries must be enforced through code structure and automated testing, not relied upon through folder organization alone. Accidental coupling—a method call that bypasses the public API, a query that reaches into another module's table—should fail a build or test, not just violate a convention.

Core Concepts

Module boundaries and bounded contexts

Module boundaries in a modular monolith should align with bounded contexts from Domain-Driven Design. Each module corresponds to one sub-domain or business capability, one module per top-level namespace or package. This alignment keeps architectural boundaries in sync with the business model: when the organization draws a line between "orders" and "inventory," the module boundary should follow that same line.

For loose coupling to hold, modules must not reference each other directly. Each module exposes a single public API or facade that contains only what other modules need; all internal domain models, repositories, and services remain private. Dependency injection resolves these interfaces at runtime.

Communication patterns

Modular monoliths support two primary communication patterns between modules:

  • Synchronous in-process method calls through public APIs. Fast and simple, but creates implicit coupling if overused.
  • Asynchronous in-memory event buses or the outbox pattern. Decouples modules temporally but adds complexity.

Synchronous calls are the right starting point. Asynchronous communication should be adopted when modules need temporal or logical decoupling—for instance, when a downstream module should not be blocked by an upstream module's latency.

Events themselves have a critical distinction: domain events are internal to a module and represent state changes within its bounded context; integration events are public contracts published at module boundaries for consumption by other modules. Conflating the two creates unintended coupling where consumers depend on internal domain event structure.

Data isolation

Each module should maintain separate database schemas—either within a single database instance or across separate databases—with one critical rule: no module may directly access another module's database tables. This is often the hardest boundary to enforce because shared tables feel cheap and expedient initially.

PostgreSQL schemas combined with role-based access control provide a practical enforcement mechanism: each module gets its own schema and a database role with privileges scoped only to that schema. The database itself then prevents accidental cross-module data access at runtime, achievable within a single PostgreSQL instance.

When module B needs to read data owned by module A, the recommended pattern is for module A to publish domain events; module B subscribes and maintains a read-optimized projection (materialized view or denormalized table) within its own schema. The projection is read-only from module B's perspective, making eventual consistency acceptable for query workloads.

The shared database trap

The decision to "share the database for now" tends to become permanent. Once teams write queries that span modules' tables, triggers that depend on that sharing, and application logic that relies on implicit cross-module transactions, the coupling becomes so embedded that separating schemas later requires months of refactoring. Real-world teams have struggled for three or more years with this problem. Enforcing data isolation from the start is cheaper than untangling permanent coupling.

Components & Structure

Practical implementation patterns

A modular monolith is implemented using consistent structural patterns:

  • Host component. An entry point that bootstraps the application and its dependency injection container. The host is the only component that references all modules; it wires them together without those modules knowing about each other.
  • Isolated modules. Each module manages its own persistence, its own internal services, and exposes only its public API surface. Module internals are hidden by package visibility or project-level access control.
  • Vertical Slice Architecture. A common organizational pattern where each slice contains all necessary components for a single feature—request handling, domain logic, persistence—in a self-contained unit. This keeps modules decoupled and aligns with how AI tools work best within bounded contexts.

Boundary enforcement mechanisms

Effective enforcement requires more than folder structure. Mechanisms include:

Architecture tests can fail an AI-generated pull request that crosses a boundary just as readily as a human-written one.

Mechanism & Process

How in-process isolation works

In a method-call based modular monolith, module boundaries are enforced by visibility rules enforced at compile time and by architecture tests at build time. The JVM provides an especially strong version of this: Java's module system enforces package encapsulation at both compile-time and runtime through the JVM itself, making strong encapsulation impossible to bypass through reflection hacks. Modules define a public API through exports directives; everything else is hidden by default.

An alternative model uses process-based isolation rather than visibility rules. In BEAM-based systems (Erlang, Elixir), processes are isolated entities where code execution occurs, and crossing module boundaries requires explicit message passing with associated runtime cost. This makes accidental coupling visible and measurable through observability metrics rather than hidden in a call graph. The actor model eliminates shared state by design: each actor encapsulates its state, and state is modified exclusively through serialized message processing, removing the need for locks.

The BEAM adds additional runtime properties: per-process garbage collection means each process has its own garbage collector, avoiding global GC pauses that would affect all processes. And the same abstractions—processes, message passing, supervision trees—extend naturally across node boundaries with location transparency, enabling a modular monolith to distribute transparently if needed without changing module-level code.

ACID transactions across modules

A shared database enables ACID transactions that span multiple module schemas atomically, allowing operations affecting multiple modules to commit or roll back as a single unit. This is a genuine operational advantage over microservices, where multi-domain operations require eventually-consistent compensation logic (sagas). The trade-off is coupling: exploiting cross-module transactions creates hidden dependencies on a specific transaction ordering and consistency contract that becomes expensive to untangle during future extraction.

Variants & Subtypes

Modular monoliths differ primarily in how they enforce boundaries:

  • Package/namespace visibility enforcement (most common). Language-level access modifiers—Java's package-private, Python's convention-based private members, C#'s internal keyword—limit what one namespace can see of another. Reinforced with architecture tests.
  • Separate library/project per module. Each module is a separate build artifact. Import boundaries are enforced by the build system. Used in Nx monorepos for TypeScript/JavaScript projects.
  • Process-based isolation (BEAM, Akka). Modules are heavyweight actors or supervision trees. Crossing a boundary requires message passing. Isolation and failure handling are enforced by the runtime.
  • Rails Engines (Shopify's approach). Rails Engines are repurposed as mini-applications with isolation, ownership, and static analysis enforcement via Packwerk.

Notable Examples

Shopify

Shopify's Rails monolith contains 2.8 million lines of code and 500,000 commits. It handled 173 billion requests on Black Friday 2024, peaking at 284 million requests per minute, processing 12 terabytes of traffic per minute through its edge. Rather than migrating to microservices, Shopify invested in domain modeling and database sharding by shop_id, maintaining deploy velocity despite rapid headcount growth.

Shopify also deployed Shopify Magic, an internal AI assistant, directly within this modular codebase. The modular structure with bounded contexts and clear internal service boundaries enabled the assistant to generate substantial features without producing the accumulated technical debt that a less-structured codebase would absorb invisibly. Packwerk enforces dependency boundaries between components, providing automatic safety checks for AI-generated code.

Amazon Prime Video

Amazon Prime Video achieved a 90% cost reduction by eliminating intermediate S3 storage and expensive API calls between microservices in its video quality analysis pipeline, collapsing what had been separate services into a single modular application. This is the most widely cited data point of the microservices consolidation trend.

Spring Modulith

Spring Modulith reached General Availability at version 1.4 on March 27, 2026, built on Spring Boot 3.5 and Java 21. It provides @ApplicationModule annotations, event externalization with a transactional outbox pattern, a documentation generator that creates architecture diagrams from code structure, observability integration, and runtime structure verification. Enabling spring.modulith.runtime-verification-enabled triggers automatic scanning and validation of module boundaries at startup.

Reception & Influence

The post-microservices backlash

Between 2014 and 2022, microservices were the default architectural choice for any application expected to grow. By 2024–2026, the accumulated evidence reversed this default. 42% of organizations are actively consolidating microservices back into larger architectural units, driven primarily by operational complexity exceeding anticipated benefits.

The economics are stark. Microservices infrastructure costs $40,000–$65,000 per month at equivalent functional scale, versus $15,000 for monoliths—a 3.75× to 6× multiplier driven by service mesh tooling, inter-service communication overhead, and resource isolation requirements. 60% of teams report regretting microservices adoption for small-to-medium-sized applications.

The 2024–2026 industry consensus has shifted to a hybrid framing: keep a modular monolith core, extract specific high-traffic modules or organizationally distinct services only when traffic patterns and organizational structure justify the work, and treat full microservices as a destination earned by demonstrated need.

Modular monoliths and AI-assisted development

The rise of AI coding tools has introduced a new reason to care about module boundaries. LLMs exhibit "boundary blindness"—they treat code visibility as permission to cross module boundaries. When code from different modules is visible in the same context window, the model may generate changes that couple previously separate concerns. Encapsulation is an agreement that LLMs do not inherently respect without technical enforcement.

AI-generated code shows systematic architectural problems: an 8-fold increase in code duplication, "god objects" with too many responsibilities, and highly coupled structures where modules depend on each other's internal details. Refactored code as a share of all changes declined from 25% in 2021 to under 10% in 2024, a trend correlated with AI adoption.

Modular boundaries directly address this. Architecture tests fail when AI-generated code crosses a module boundary, making invisible architectural drift detectable before it accumulates. Vertical Slice Architecture is particularly AI-friendly because each slice contains all necessary components for a single feature, giving the model bounded context without requiring visibility of the entire codebase. Modern LLMs with 128K–1M token context windows favor monorepos with explicit module boundaries: the monorepo allows the model to see related code while module boundaries prevent architectural violations.

Controversies & Debates

Stepping stone or destination?

The most persistent debate is whether a modular monolith is a temporary state on the way to microservices, or a long-term architectural destination in its own right. Organizations under 15 engineers, SaaS products under $10M ARR, or systems without multiple independent scaling hotspots are better served by a well-structured modular monolith than by migrating to microservices. Shopify is the primary evidence that modular monoliths can remain the destination even at extreme scale.

The counterposition is that modular monoliths still struggle when strict fault isolation or independent scaling across many modules is genuinely required. A single deployment unit means that a module crash can take down unrelated modules, and that scaling an IO-heavy module requires scaling the entire application.

How real are modular boundaries in practice?

Extracting modules from a modular monolith into microservices is harder in practice than architectural theory suggests. Even deliberately modular systems exhibit hidden coupling that only becomes visible during extraction attempts, primarily across shared databases, transaction patterns, and ambient context. Database decomposition is the primary technical barrier: splitting a database requires careful handling of data synchronization, transactional integrity, joins, and latency, and frequently the point where extraction attempts fail.

This is partly an argument for better upfront boundary design and partly evidence that the clean boundary promised by modular architecture is harder to maintain under production pressure than the diagram suggests.

When to Migrate to Microservices

Microservices extraction requires operational maturity as a prerequisite: CI/CD, observability, SLOs, and runbooks. Teams lacking this infrastructure should remain modular monoliths while building platform and operational practices rather than attempting extraction.

A realistic extraction timeline for a medium codebase follows a 12-month progression:

  • Months 0–2: Discovery and domain mapping
  • Months 2–6: Modularization—reorganizing code and adding explicit interfaces
  • Months 6–8: First extraction of a small, well-bounded module (auth is commonly recommended) using the Strangler Fig pattern, which allows the new service to run parallel to the monolith and gradually intercept traffic
  • Months 9–12: Additional extractions of modules where data and scale justify the work

Clean module boundaries map directly to microservice candidates when you're ready to extract. A well-designed modular monolith is already halfway toward a microservices architecture; the boundaries serve as blueprints.

Scaling a Modular Monolith

A common objection is that monoliths cannot scale. The evidence from production systems suggests otherwise for most workloads.

Modular monoliths can scale using vertical scaling, route-based scaling to isolate read-heavy endpoints, read replicas, caching strategies, and asynchronous in-process queues before microservice extraction becomes necessary. Organizations should remain in the modular monolith phase when the bottleneck is team coordination or codebase complexity rather than independent scaling or technology heterogeneity needs.

Shopify's approach was explicit: scale through database sharding by shop_id and disciplined data domain ownership, rather than service extraction. The result was maintained deploy velocity at 2.8 million lines and hundreds of engineers.

The cases where modular monoliths genuinely hit limits are systems requiring strict fault isolation across many independent modules, or systems with multiple modules that need fundamentally different technology stacks or independent deployment cycles driven by distinct organizational units.

Key Takeaways

  1. A modular monolith balances simplicity and scale Single-unit deployment with enforced logical boundaries enables teams of 1-50 to retain operational simplicity while avoiding the overhead of microservices. It retains in-process communication speed while providing ownership clarity and parallel development patterns.
  2. Industry consensus has shifted toward modular monoliths as the default 42% of organizations that adopted microservices are consolidating back into larger deployable units. Microservices cost 3.75-6x more to operate, and 60% of teams report regretting adoption for small-to-medium applications.
  3. Module boundaries must be enforced, not just followed by convention Package visibility, separate projects, architecture tests, and database schema isolation prevent accidental coupling. Tools like ArchUnit and Packwerk make boundary violations detectable in CI/CD before they accumulate.
  4. Data isolation is the hardest boundary to enforce Shared databases create permanent coupling once tables are crossed. Using separate schemas per module with role-based access control, or having modules maintain projections of data they need, prevents this trap.
  5. AI coding tools benefit from strong module boundaries LLMs exhibit boundary blindness and generate 8x more duplication and coupling when they have visibility across modules. Architecture tests fail AI-generated code that crosses boundaries, making invisible drift detectable.

Further Exploration

Core concepts

Migration & scaling

Industry case studies

Tooling & implementation

AI-assisted development