← Back to list
SW Engineering & Management
#모듈러모놀리스#마이크로서비스#모듈화#경계컨텍스트#아키텍처패턴
Last updated · 2026-11-01

Modular Monolith Architecture

1. Overview

A modular monolith is an architectural style that, within a single deployment unit (a single process), divides the business domain into independent modules with clear boundaries and forces modules to interact only through public contracts (public APIs) and events — pursuing both the operational simplicity of a monolith and the loose coupling of microservices at the same time.

Over the past decade or so, many organizations decomposed their systems into microservices in the name of scalability and team autonomy, yet a significant number took on costs instead of the expected benefits: the latency and failures of network calls, distributed transactions, inter-service version compatibility, and the complexity of observability and tracing. In particular, when an early-stage product whose domain boundaries had not yet stabilized was split prematurely, it frequently ended up as a "distributed monolith" in which a single feature change spread into simultaneous deployments across multiple services. Amid this reflection, an approach that keeps the simplicity of a single deployment while modularizing the internal structure as rigorously as microservices was revisited, and this is the modular monolith.

The problem with the traditional monolith lies not in "single deployment" but in the "absence of internal boundaries." A codebase divided only by layers degenerates over time into a "big ball of mud" in which one can no longer trace which feature touches which data. Instead of making the deployment boundary singular, the modular monolith erects logical boundaries along domain units and hides each module's internal implementation from the outside to control coupling. In other words, the key mindset is to treat "integration of deployment" and "separation of design" as distinct concerns.

The necessity of this approach is judged by the direction of change. If domain boundaries are still fluid, the team size is a few dozen people or fewer, and the operations organization has not yet reached the maturity to handle distributed systems, then a modular monolith is reasonable. Conversely, if traffic surges on only a specific module so that independent scaling is essential, or if different languages, runtimes, and deployment cadences are strongly required, then carving out just that module as a service is the better choice. A professional engineer must be able to explain not the dichotomy of "monolith versus microservices," but where one should stand along the axes of change isolation, scaling demands, and organizational maturity.

2. Overall Structure and Core Design Principles

A modular monolith is a structure in which several domain modules coexist within a single executable (or container), yet each module hides its internals behind its public contract. Viewed from the outside it is a single application, but viewed from the inside it is a collection of clearly separated "service candidates." The diagram below shows the order, payment, shipping, and inventory modules connected within a single process only through public APIs and domain events.

flowchart TB
    subgraph APP["Single deployment unit (one process)"]
        direction LR
        O["Order module\nPublic API + hidden internals"]
        P["Payment module\nPublic API + hidden internals"]
        S["Shipping module\nPublic API + hidden internals"]
        I["Inventory module\nPublic API + hidden internals"]
        BUS["In-process event bus"]
        O -->|Public API call| P
        O -->|OrderConfirmed event| BUS
        BUS -->|subscribe| S
        BUS -->|subscribe| I
    end
    UI["Web / mobile client"] --> O
    O --> DBO[("Order schema")]
    P --> DBP[("Payment schema")]
    S --> DBS[("Shipping schema")]
    I --> DBI[("Inventory schema")]

The most important rule in this structure is that a module may not directly reference another module's internal classes or tables. If the order module needs payment, it calls only the payment module's public API (e.g., PaymentPort.authorize(...)) and cannot know the payment internals' authorization logic or storage structure. This way, even if the payment module is fully rewritten or later split out into a separate service, the order module is unaffected as long as the public contract is maintained.

The second principle is the separation of data ownership. Even if all modules share a single database, each module has write access only to its own schema (or set of tables) and does not directly join another module's tables. When another module's data is needed, instead of a join it is queried through the public API or received via events and replicated into its own read model. If this rule breaks down, the system immediately regresses to a traditional monolith in which "everything is connected at the DB level," so it is the most sensitive point that determines the success or failure of a modular monolith.

The third principle is the control of dependency direction. If dependencies between modules form cycles, separation becomes impossible, so one distinguishes a shared kernel from domain-specific modules and designs the dependency graph to be acyclic. For example, common identifiers, value objects, and event definitions are placed in a thin shared module, while business rules reside only in each domain module.

2.1 Identifying Module Boundaries

Where to draw module boundaries is not a question of architecture but of the domain. The bounded context of domain-driven design (DDD) is the strongest candidate, and the point where the same term carries different meanings (for example, "customer" in ordering versus "customer" in marketing) is precisely the line that separates modules. If boundaries are drawn wrongly, one feature change comes to touch multiple modules simultaneously and coupling actually increases, so the principle is to group cohesive concepts that change together into a single module.

In practice it is safer to take an evolutionary approach: draw boundaries loosely at first, observe which modules the change logs and commits cluster around, and readjust the boundaries. An attempt to design perfect boundaries in one shot carries a high risk of fossilizing wrong boundaries during the early period when domain understanding is insufficient.

2.2 Means of Boundary Enforcement

Boundaries are maintained only when they are enforced by tools, not by documents or verbal promises. In the Java camp, Spring Modulith or ArchUnit fixes rules as tests such as "if module A references module B's internal package, the build fails." In .NET, one splits project references into module units and restricts internal types as internal, and in Java 9 and above the module system (JPMS) or package access control narrows the public surface. Without such automated verification, developers take the easy shortcut (direct reference) amid busy schedules and the boundaries disappear within a few months.

3. Inter-Module Interaction and Internal Structure

Inter-module communication is largely divided into synchronous calls and asynchronous events, and each choice directly affects coupling and consistency. The diagram below expresses the flow in which a single order creation propagates to the payment, inventory, and shipping modules, using a mix of synchronous and asynchronous interactions.

sequenceDiagram
    participant C as Client
    participant O as Order module
    participant P as Payment module
    participant B as Event bus
    participant I as Inventory module
    participant S as Shipping module
    C->>O: Create order request
    O->>P: Public API call (authorize payment)
    P-->>O: Return authorization result
    O->>O: Persist confirmed order
    O->>B: Publish OrderConfirmed event
    B->>I: Process inventory deduction
    B->>S: Process shipping preparation
    O-->>C: Respond with order number

Synchronous calls are used when an immediate result is required. Because the order must know whether payment is authorized before it is confirmed, the order module calls the payment module's public API in-process and waits for the result. Because in-process calls do not traverse the network, latency is virtually nil and processing can occur within the same transaction boundary, avoiding the complexity of distributed transactions — a decisive advantage over microservices.

Asynchronous events are used when there is no need to wait for a result immediately and when the sending module need not know of the receiving module's existence. After an order is confirmed, inventory deduction and shipping preparation are not the order module's concern, so the order module publishes only the OrderConfirmed event and does not know who subscribes to it. This way, even if a "point accrual" module is added later, modules that subscribe to the event are simply added without changing the order module's code, so the open-closed principle is realized at the module level.

A design pitfall to watch here is replacing events with simple method calls, leaning on the convenience of being "in the same process." Even for in-process events, the publish-subscribe contract must be made explicit so that, when that module is later split out as a separate service, swapping the events for a message broker (e.g., Kafka) becomes a local change. Conversely, if events are overused so that even every synchronous flow is handled asynchronously, a paradox arises in which one needlessly accepts eventual consistency within a single process, so balance is needed.

3.1 Transactions and Consistency

A great advantage of the modular monolith is the ability to bundle changes across several modules into a single local transaction. If order persistence and the payment record are committed within the same transaction, data inconsistency due to partial failure can be prevented at the root, so one easily attains the consistency that microservices achieve only with difficulty through Sagas and compensating transactions. That said, the event-based asynchronous portion is still eventual consistency, so for the atomicity of event publication and state change it is safer to apply a pattern similar to the transactional outbox inside the module.

3.2 Internal Package Structure

Each module is commonly divided internally into api (public contract), domain (business rules), infrastructure (storage and external integration), and internal (hidden implementation). What is exposed is only the interfaces, events, and DTOs of the api package, and the rest is blocked from reference outside the module. This structure can be seen as the hexagonal architecture (ports and adapters) applied repeatedly at the module level, and it keeps business rules from being contaminated by technical detail even within one module.

4. Comparison with Microservices and the Traditional Monolith

The three architectures are often misunderstood as a hierarchy of superiority, but in reality they are options that optimize different constraints. The core difference arises from "the number of deployment units" and "the means of boundary enforcement," and that difference leads to a trade-off between operational cost and change flexibility.

Category Traditional monolith Modular monolith Microservices
Deployment units 1 1 N
Module boundaries Vague (layer-centric) Clear (domain, tool-enforced) Physically separated
Inter-module comms Direct calls, shared tables Public API, in-process events Network (REST, gRPC, messaging)
Transactions Single local Single local Distributed (Saga, compensation)
Independent scaling Impossible Impossible (whole-app only) Possible per module
Operational complexity Low Low High
Team autonomy Low Medium High

As the table shows, the modular monolith differs decisively from the traditional monolith in that it keeps operational complexity low while making boundaries clear. On the other hand, it forgoes the "per-module independent scaling" and "language/runtime heterogeneity" that microservices provide. Therefore, the moment traffic concentrates on a particular module and independent scaling becomes a core requirement is precisely the signal to transition from a modular monolith to microservices.

Real-world cases make this trade-off vivid. The e-commerce platform Shopify, instead of splitting its large Ruby on Rails codebase into microservices, restructured it into a boundary-enforced modular monolith, preserving the benefits of single deployment while letting a development organization of thousands work in parallel. The Amazon Prime Video team — not a Netflix affiliate — disclosed in 2023 that it consolidated its video-quality monitoring pipeline from distributed microservices into a single application and cut infrastructure cost by about 90%, a representative case showing that excessive distribution can instead breed inefficiency. The customer data platform Segment likewise shared its experience of being unable to bear the operational burden of microservices that had grown into the hundreds and consolidating back into a monolith; the common lesson of these transitions is that "separation is not free, and it is not too late to defer it until boundaries have stabilized."

5. Deep Dive: Transition Strategy and Recent Trends

A place where the modular monolith is especially powerful is its ability to function as an "intermediate station" on the way to microservices. The "Monolith First" principle advanced by Martin Fowler advises that, because prematurely splitting services early on — when domain boundaries are uncertain — physically fossilizes wrong boundaries, one should first refine boundaries within a single deployment unit and carve out only the necessary modules as services after those boundaries have stabilized. The modular monolith becomes a natural foundation for this strategy. Because inter-module communication is already abstracted into public APIs and events, at the time of separation the work of turning in-process calls into remote calls and in-process events into a message broker is confined to the module boundary.

The tooling ecosystem has also matured to support this approach. The Spring camp's Spring Modulith makes module boundaries explicit, verifies violations with tests, and supports inter-module events and documentation. ArchUnit asserts package dependency rules in code to block boundary violations in CI, and .NET's NDepend and architecture test libraries play a similar role. As this "break the boundary and the build breaks" automation becomes widespread, the modular monolith is no longer a fragile pattern that depends only on discipline but a practicable architecture sustained by tools.

Recently, in cloud environments, even a single deployment commonly supports horizontal scaling (the same image across multiple instances) and zero-downtime deployment, so the notion that "a monolith cannot scale" has been considerably softened. Operating a modular monolith atop serverless or container orchestration can handle a considerable scale by replicating the whole application to distribute traffic. In the end, a "hybrid" form that selectively separates only the few modules that truly need independent deployment and independent scaling is settling in as the realistic governing structure.

6. Considerations and Implications

First, automation of boundary enforcement determines success or failure. A modular monolith is maintained only when boundaries are kept by build and test tools, not by developers' goodwill. Architecture tests such as ArchUnit and Spring Modulith should be placed as mandatory gates in the CI pipeline, and the scheme that blocks boundary violations before they are merged must be prepared together with the architecture design.

Second, data ownership must be nailed down at the design stage. The most common failure begins when one directly joins another module's tables "for performance." Separating schemas per module and prohibiting cross joins should be codified as conventions, and guidance should direct that when another module's data is needed it be resolved by public API query or event-based read-model replication, so that the possibility of separation is preserved in the long run.

Third, it is reasonable to defer the transition until boundaries have stabilized. Deciding to go to microservices from the start is the paradox of choosing the most expensive structure at the moment when domain understanding is weakest. Starting with a modular monolith, observing change patterns and traffic characteristics, and selectively separating only the modules for which independent scaling, independent deployment, and technological heterogeneity are actually required is a strategy that lowers both cost and risk.

Fourth, organizational structure and architecture must be designed together. By Conway's law, system structure resembles the communication structure of the organization. Aligning module boundaries with team responsibility boundaries in a modular monolith clarifies code ownership and reduces change conflicts, but since a single repository and deployment are shared, deployment coordination and code review processes must be agreed upon across teams for the benefits of integration to be maintained.

Fifth, observability and per-module metrics should be designed together. Even in a single process, measuring processing time, error rate, and event backlog separately per module makes it possible to judge from data which module is the bottleneck and which module should be separated first, providing the basis for transition decisions.

References


In one line: A modular monolith keeps the simplicity of a single deployment while connecting domain modules only through public APIs and events and enforcing boundaries with tools, so it obtains the loose coupling of microservices at low cost and lets only the necessary modules be split out as services later.