Tags: architecture, backend, ddd, design, java, csharp
Level: intermediate
TL;DR: A system deployed as a single application process but strictly organized into independent, isolated modules internally, offering microservices-like boundaries without distributed complexity.
Deployed as a single unit, avoiding network partitions and distributed system overhead.
Internally divided into strictly isolated modules, often mapped to Bounded Contexts.
Modules communicate exclusively through well-defined public interfaces or in-memory events.
Each module manages its own data persistence to prevent direct cross-module database coupling.
Keeps deployment, testing, and operational infrastructure simple and monolithic.
Provides a clear migration path to microservices if specific modules require independent scaling later.
Requires strict architectural discipline to prevent degradation into a tightly coupled big ball of mud.
Q: Why choose a modular monolith instead of starting with microservices?R: I prefer it because it prevents the premature complexity of distributed transactions, network latency, and complex deployments, while still enforcing clean team and code boundaries.
Q: How do you enforce boundaries between modules effectively?R: I rely on architectural fitness functions like ArchUnit in the CI pipeline, package visibility modifiers, and strict dependency injection to block illegal cross-module references.
Q: How should modules share data if cross-database joins are forbidden?R: I expose necessary data through a module's public API or use domain events to replicate read-only projections of the required data into the consuming module's schema.
Performing direct database joins across tables belonging to different modules.
Leaking internal domain models or entities into the public API contracts of a module.
Failing to automate boundary enforcement, allowing developers to bypass interfaces for convenience.
Creating a shared "common" module that becomes a dumping ground for tightly coupled utilities.
// Enforcing boundaries with ArchUnit
@Test
public void ordersModuleShouldNotDependOnInventoryInternals() {
noClasses()
.that().resideInAPackage("..orders..")
.should().dependOnClassesThat().resideInAPackage("..inventory.internal..")
.check(importedClasses);
}
[ ] Deep dive into Domain-Driven Design Context Mapping patterns.
[ ] Explore outbox pattern implementations for in-memory event publishing between modules.