Overview
A modular monolith is a single deployable application divided into well-defined modules, each owning its domain logic and data, and communicating only through explicit public interfaces. It keeps the operational simplicity of a monolith while gaining many of the design benefits of microservices: clear ownership, encapsulation, and independent development.
Boundaries are enforced with code structure and tooling (package visibility, dependency rules, architecture tests), and each module ideally owns its own schema or tables. If a module later needs independent scaling or deployment, it can be extracted into a service with much less effort because its boundary already exists.
Departments share one building (deployment) and utilities, but each has its own space, staff, and filing cabinets. They interact through front desks (public APIs), not by walking into each other's offices and editing files.
When to use it
- Growing teams that need ownership boundaries without distributed complexity.
- Preparing for a possible future move to microservices.
- Refactoring a tangled monolith incrementally.
- Domains with clear bounded contexts.
Where it shows up in interviews
Recognize it when: the monolith is tangled but microservices seem too costly.
- Refactor a legacy e-commerce app
- Design architecture for a 30-person team
Where it is used in real software
Shopify split its Rails monolith into components with enforced boundaries using its Packwerk tool.
A Spring project for building modular monoliths with verified module boundaries and events.
JPMS and ArchUnit tests prevent illegal dependencies between packages.
Key terms
- Module
- Cohesive unit owning a business capability.
- Public API
- The only way other modules may interact.
- Data ownership
- Only the owning module reads and writes its tables.
- In-process events
- Modules communicate asynchronously within the app.
- Architecture tests
- Automated checks for dependency rules.
How it works, step by step
- 1Identify bounded contexts
Catalog, ordering, payments, shipping.
- 2Create modules with public APIs
Hide internals.
- 3Split data ownership
Separate schemas or table prefixes per module.
- 4Enforce rules in CI
Fail builds on forbidden imports.
- 5Use events for side effects
Decouple modules, easing future extraction.
Monolith vs modular monolith vs microservices
Three architectures
| Aspect | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | One unit | One unit | Many units |
| Boundaries | Weak | Enforced in code | Enforced by network |
| Data | Shared tables | Owned per module | Database per service |
| Communication | Any function call | Public APIs, in-process events | HTTP, gRPC, messaging |
| Operational complexity | Low | Low | High |
NOWAspect: Deployment | Monolith: One unit | Modular monolith: One unit | Microservices: Many units
The modular monolith is often the best of both worlds until a module truly needs separate deployment or scaling.
Implementation
// orders module: public APIpackage com.shop.orders; public interface OrderApi { OrderId placeOrder(CustomerId customer, List<LineItem> items);} // payments module depends only on the public API and events, never on orders.internal.*package com.shop.payments; @Componentclass PaymentOnOrderPlaced { @EventListener void on(OrderPlaced event) { charges.create(event.orderId(), event.totalCents()); }} // ArchUnit test enforcing boundaries@ArchTeststatic final ArchRule no_internal_access = noClasses() .that().resideOutsideOfPackage("com.shop.orders..") .should().accessClassesThat().resideInAPackage("com.shop.orders.internal..");Complexity and performance
No network overhead.
Boundary already exists.
Trade-offs
All modules still release together and share runtime resources; one memory leak affects all.
Boundaries are only as strong as the tooling and reviews enforcing them.
Variants and related techniques
Stronger data isolation in one database.
Modules loaded dynamically.
Common mistakes
- Modules reading each other's tables.
Fix: Access data only through the owning module's API.
- Shared 'common' module that grows into everything.
Fix: Keep shared code minimal and technical.
Interview questions
What is a modular monolith and why choose it?
A single deployable with strongly enforced module boundaries and data ownership. It gives clear ownership and maintainability without the network, deployment, and consistency costs of microservices, and makes later extraction easier.
How do you enforce module boundaries?
Package visibility, explicit public APIs, per-module schemas, architecture tests in CI (ArchUnit, Packwerk), and code review ownership rules.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Split a monolith into 4 modules with APIs | Medium | Boundaries. |
| Plan extraction of payments into a service | Hard | Strangler pattern. |