Software ArchitectureSystem Design

The Modular Monolith: The Architect's Sweet Spot for 2026 Engineering Teams

TT
TopicTrick Team
The Modular Monolith: The Architect's Sweet Spot for 2026 Engineering Teams

The Modular Monolith: The Architect's Sweet Spot for 2026 Engineering Teams


Table of Contents


What Separates a Modular Monolith from a Big Ball of Mud?

The difference is enforcement. Both are single deployable units. But in a Big Ball of Mud, any class can call any other class without restriction. In a Modular Monolith:

  1. Each module owns its own domain and data — no module accesses another module's internal classes or tables directly
  2. Cross-module communication happens only through published, stable public APIs
  3. These boundaries are enforced by tooling — tests fail, the build breaks, or the compiler rejects cross-boundary imports

Without enforcement, a modular monolith decays into a Big Ball of Mud within 9–18 months as developers take "one-off" shortcuts.


Defining Module Boundaries with Domain-Driven Design

Use Bounded Contexts from Domain-Driven Design to identify module boundaries. A bounded context is a business area where a specific set of concepts and rules apply consistently.

For an e-commerce platform:

Module (Bounded Context)OwnsDoes NOT own
IdentityUsers, sessions, roles, permissionsOrders, payments
CatalogProducts, categories, pricingStock levels, orders
InventoryStock levels, warehouse locationsPricing, users
OrdersOrder lifecycle, cart, checkoutUser credentials, pricing rules
PaymentsPayment methods, transactions, refundsOrder creation, inventory
NotificationsEmail, SMS, push notification deliveryBusiness decisions

The key insight: When the Orders module needs to know if a product is in stock, it calls Inventory.isAvailable(productId) through a well-defined interface — it never reads the inventory_items table directly.


Enforcing Isolation: Language-Level Tools

Go: internal Packages

Go's internal directory convention is enforced by the compiler — one of the most powerful built-in module isolation tools:

text

Java: ArchUnit Tests

ArchUnit lets you write architecture rules as JUnit tests — violations fail the build:

java

Java: Jigsaw Modules (module-info.java)

Java's module system enforces at the compiler and JVM level:

java

Module Public API Design

Each module should expose a clean Facade — a single class that represents everything the module offers the outside world:

java

Cross-Module Communication: Sync vs Async

Synchronous (Direct Call): Use when the operation needs an immediate response

java

Asynchronous (In-Memory Event Bus): Use when the operation is a side effect

java

Why in-process events over Kafka? Because you're in the same process, events are delivered synchronously within the same transaction (with Spring's @TransactionalEventListener) or instantly after commit — no Kafka broker required, no eventual consistency, no message ordering issues.


Database Strategy: From Shared Tables to Schema Isolation

text
sql

Comparison Matrix

FeatureStandard MonolithModular MonolithMicroservices
Deployment complexityLowLowVery High (Kubernetes)
Development velocityHigh initially, degradesHigh (sustained)High for large teams
Internal couplingHigh (no walls)Low (enforced)Low (physical separation)
TestingOne test suiteOne test suitePer-service + contract tests
Transaction supportACID (shared DB)ACID or schema-isolatedSaga pattern (complex)
DebuggingStack traceStack traceDistributed tracing required
Team size sweet spot1–15 engineers10–100 engineers100+ engineers
Microservice migrationHard (no boundaries)Easy (boundaries exist)Already there

When to Extract a Module into a Microservice

Extract a module when one of these is true:

  1. Independent scaling need: The module's load is 10× other modules, and you can save significant cost by scaling only it
  2. Team autonomy: The module is owned by a separate team that needs to deploy independently multiple times per day
  3. Technology isolation: The module genuinely benefits from a different runtime (e.g., extracting video processing to a Python service with GPU support)
  4. Compliance: Regulatory requirements demand physical isolation (PCI-DSS for payment processing data)

Do NOT extract because:

  • You think it might scale in the future
  • The module is "logically separate" (that's what module boundaries are for — they don't require physical separation)
  • You want to use a different language for its own sake

Frequently Asked Questions

How big should a module be? A module should correspond to one Bounded Context — a cohesive set of business concepts with a clear owner. Practically, this often maps to one "product team" or one vertical of the business. A module with more than 50,000 lines of code might be a signal it should be further divided into sub-modules. A module with under 500 lines is probably too fine-grained.

Should each module have its own database schema even in development? Yes, at minimum use table prefixes. Schema isolation in development prevents the temptation of cross-module SQL joins — which are the most common way boundaries erode. The discipline of using only public APIs for cross-module data access must be practiced from day one.


Key Takeaway

The Modular Monolith is the architecture that scales with your team's growth. At 10 engineers, you get all the benefits of a monolith with the organizational structure of a well-designed system. At 80 engineers, the strict boundaries you enforced from day one allow you to extract modules into microservices one at a time — without a big-bang rewrite. The discipline of enforcement tooling (ArchUnit, Go internal, module-info.java) is the non-negotiable investment that makes this architecture sustainable.

Read next: When Microservices Hurt: Anti-patterns and Pitfalls →


Part of the Software Architecture Hub — comprehensive guides from architectural foundations to advanced distributed systems patterns.