Modular Monolith vs Microservices — Start Modular (2026)
In this tutorial, you'll learn the modular monolith pattern — organizing a single-deployment application into well-defined modules with clear boundaries, and when to extract those modules into microservices. Why does this matter? Microservices introduce massive complexity (network latency, distributed transactions, service discovery, operational overhead) that most applications never need. A modular monolith gives you the architectural benefits of microservices without the operational cost. Real-world use: Shopify, Klarna, and Segment all use modular monoliths that serve millions of users with a single deployment.
What Is a Modular Monolith?
A modular monolith is a single deployment unit with logically separated modules. Each module has its own domain, data, and public API — just like a microservice — but everything runs in the same process. Modules communicate through explicit interfaces (in-process), not network calls.
This is different from a "big ball of mud" monolith where concerns are tangled. In a modular monolith, module boundaries are enforced at the code level.
graph TB
subgraph ModularMonolith[Modular Monolith]
API[API Layer / Entry Point]
subgraph OrdersModule[Orders Module]
O_API[Public API]
O_CORE[Domain Logic]
O_DB[(Orders DB Schema)]
end
subgraph InventoryModule[Inventory Module]
I_API[Public API]
I_CORE[Domain Logic]
I_DB[(Inventory DB Schema)]
end
subgraph UsersModule[Users Module]
U_API[Public API]
U_CORE[Domain Logic]
U_DB[(Users DB Schema)]
end
API --> O_API
API --> I_API
API --> U_API
O_API --> I_API
O_API --> U_API
end
subgraph Future[Extractable Services]
OrdersSvc[Orders Microservice]
InventorySvc[Inventory Microservice]
UsersSvc[Users Microservice]
end
OrdersModule -.-> OrdersSvc
InventoryModule -.-> InventorySvc
UsersModule -.-> UsersSvc
style ModularMonolith fill:#4A90D9,color:#fff
style Future fill:#95a5a6,color:#fff
How Modular Monoliths Work
Module Boundaries
Each module is a self-contained unit with:
- Public interface — what other modules can call
- Internal implementation — hidden from other modules
- Private data — tables or schemas owned exclusively by the module
- Domain events — emitted when something interesting happens
# --- Orders Module (public API) ---
class OrdersModule:
def create_order(self, user_id: str, items: list[OrderItem]) -> Order:
# Uses Inventory Module through its public API
inventory = self.inventory.check_availability(items)
if not inventory.available:
raise InventoryError("Items not available")
order = Order(user_id=user_id, items=items)
self.orders_repo.save(order)
self.event_bus.publish(OrderCreated(order.id))
return order
def get_order(self, order_id: str) -> Order:
return self.orders_repo.find_by_id(order_id)
# --- Inventory Module (public API) ---
class InventoryModule:
def check_availability(self, items: list[OrderItem]) -> Availability:
stock = self.inventory_repo.get_all_stock()
return Availability(
available=all(
stock.get(i.sku, 0) >= i.quantity for i in items
)
)
def reserve_items(self, items: list[OrderItem]) -> None:
for item in items:
self.inventory_repo.decrement(item.sku, item.quantity)
Communication
Modules communicate in-process through explicit interfaces or events. This avoids network overhead:
# Event bus within the monolith
class InProcessEventBus:
def __init__(self):
self._handlers: dict[type, list[callable]] = {}
def publish(self, event: object) -> None:
for handler in self._handlers.get(type(event), []):
handler(event)
def subscribe(self, event_type: type, handler: callable) -> None:
self._handlers.setdefault(event_type, []).append(handler)
# Usage
event_bus = InProcessEventBus()
event_bus.subscribe(OrderCreated, send_confirmation_email)
event_bus.subscribe(OrderCreated, reserve_inventory)
Database Strategy
Modules can share a database instance but own separate schemas. No module directly queries another module's tables — only through that module's public API:
-- Orders module owns 'orders' schema
CREATE SCHEMA orders;
CREATE TABLE orders.orders (...);
CREATE TABLE orders.order_items (...);
-- Inventory module owns 'inventory' schema
CREATE SCHEMA inventory;
CREATE TABLE inventory.products (...);
CREATE TABLE inventory.stock_levels (...);
When to Extract to Microservices
graph LR
MM[Modular Monolith] -->|Team grows beyond
2 pizza teams| Microservices
MM -->|Module needs
independent scaling| Microservices
MM -->|Different deployment
cadence| Microservices
MM -->|Technology
divergence needed| Microservices
MM -->|Performance isolation
required| Microservices
style MM fill:#2ECC71,color:#fff
style Microservices fill:#E67E22,color:#fff
Extraction Process
- Identify module that benefits from independence
- Extract data — move its schema to a separate database
- Replace in-process calls with HTTP/gRPC/async calls
- Add service discovery and resilient communication patterns
- Deploy independently once extraction is complete
Pros and Cons
| Modular Monolith | Microservices |
|---|---|
| Single deployment — easy ops | Complex deployments — many moving parts |
| Low latency — in-process calls | Network latency — inter-service calls |
| Atomic transactions — ACID across modules | Eventual consistency — sagas, compensating transactions |
| Simple testing — one process | Complex testing — integration, contract, end-to-end tests |
| Refactoring across modules is easy | Refactoring across services requires coordination |
| Less code — no service discovery, serialization layers | More code — each service needs its own boilerplate |
| All or nothing scaling | Fine-grained scaling |
Real-World Examples
Shopify
Shopify's core runs as a monolith serving 2M+ merchants. They use modular boundaries (orders, products, payments, shipping) with strict internal APIs. They've extracted only a few services (like image processing) where scaling requirements demand it.
Klarna
Klarna's "KOH" system is a modular monolith handling millions of transactions. Modules communicate via an internal event bus with clear contracts. They extract services only when a module needs independent deployment.
Segment
Segment's pipeline engine started as a modular monolith. Each source, transformation, and destination is a module. They extracted high-throughput destinations as needed.
When to Choose Each
Choose Modular Monolith when:
- Team <10 people
- Application is <3 years old
- Transactional consistency is critical
- Deployment complexity should be minimized
Choose Microservices when:
- Multiple independent teams (>10)
- Different scaling needs per domain
- Polyglot technology stack required
- Independent release cycles needed
FAQ
Related Concepts
- Microservices Architecture — destination after extraction
- Clean Architecture — module boundaries map to layers
- Hexagonal Architecture — ports/adapters within modules
- Domain-Driven Design — bounded contexts as module boundaries
- Event-Driven Architecture — module communication via events
Practice Questions
What distinguishes a modular monolith from a traditional monolith?
List three signals that indicate it's time to extract a module into a microservice.
How does in-process communication benefit a modular monolith compared to microservices?
Why should each module own its own database schema?
What is the role of a shared kernel in a modular monolith?
Challenge
Take a traditional monolith. Identify 3-4 bounded contexts. Refactor the codebase into modules with explicit public APIs, private data, and event-based communication between modules. Ensure no module directly accesses another module's database tables.
Real-World Task
Audit an existing monolith. Count how many modules/bounded contexts exist. Check if data access crosses module boundaries. If so, add a public API to the owning module and refactor cross-module data access through it. This is the first step toward microservice readiness.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro