The Modular Monolith: A Pragmatic Alternative to Microservices
How to build a modular monolith with strict module boundaries, internal event buses, and a clear extraction path to microservices when the time comes.
Akhil Sharma
February 14, 2026
The Modular Monolith: A Pragmatic Alternative to Microservices
The microservices vs monolith debate has a missing middle: the modular monolith. It gives you the code organization and bounded context isolation of microservices without the operational complexity of distributed systems. For most teams under 50 engineers, it's the right starting architecture.
What Makes a Monolith "Modular"
A modular monolith isn't just a well-organized monolith. It has enforced boundaries between modules — not just folder conventions. The test: can you extract any module into a separate service without changing the other modules?
Directory Structure
Rule 1: Modules Communicate Through Public APIs
Each module exposes a public API — a set of functions that other modules can call. Direct imports of internal module code are forbidden.
Rule 2: No Cross-Module Database Access
Each module owns its tables. No module reads or writes another module's tables. Not even read-only access.
Advanced System Design Cohort
We build this end-to-end in the cohort.
Live sessions, real systems, your questions answered in real time. Next cohort starts 2nd July 2026 — 20 seats.
Reserve your spot →No cross-schema foreign keys. The orders.user_id column is just a UUID, not a foreign key reference to users.users.id. This is intentional — it means the orders module can be extracted to its own database without breaking referential integrity.
When you need data from another module: Call that module's public API, don't join across schemas.
Yes, this is two queries instead of one join. The performance trade-off is usually negligible for single-entity lookups. For list views, use an event-driven read model (see Rule 3).
Rule 3: Async Communication Through an Event Bus
For decoupled communication, modules publish events and subscribe to events from other modules. In a monolith, this is an in-process event bus — no Kafka needed.
When you eventually extract a module into a microservice, you replace the in-process event bus with Kafka or NATS. The handler code doesn't change — only the transport.
Enforcing Module Boundaries
Conventions alone aren't enough. Developers under deadline pressure will take shortcuts. You need automated enforcement.
Python: Import Linting
Java/Kotlin: ArchUnit
Run these tests in CI. A failing architecture test blocks the merge — no exceptions.
The Extraction Path
When a module is ready for extraction (team growth, scaling needs, deployment independence), the process is straightforward because boundaries already exist:
- Deploy the module as a separate service. The module already has its own database schema and public API.
- Replace in-process API calls with HTTP/gRPC calls. The module's
api.pybecomes a client library that calls the new service. - Replace the in-process event bus with Kafka/NATS. Event types and payloads stay the same.
- Migrate the database schema to a separate database. The schema was already isolated — just move it.
The key: this migration is incremental. You extract one module at a time, and the rest of the monolith continues working.
When to Stay Monolithic
The modular monolith is the right architecture when:
- Your team is under 50 engineers
- You don't need independent deployment of modules
- You don't need different scaling profiles per module
- You value development velocity over deployment independence
Switch to microservices when:
- Teams can't work independently due to deployment coupling
- A specific module needs to scale differently
- You need polyglot technology (one module needs Python ML, another needs Go for performance)
Start with the modular monolith. Extract modules to services when you have evidence — not speculation — that the operational complexity is justified.
More in Architecture
The Strangler Fig Pattern: Migrating Legacy Systems Incrementally
Implementing the strangler fig pattern for legacy migration with request routing, data synchronization, feature parity verification, and a realistic migration timeline.
Designing Data Pipeline Architecture for Real-Time Analytics
Real-time data pipeline design covering Lambda vs Kappa architecture, stream processing with Kafka Streams and Flink, and handling late-arriving data.