Chapter 18: The Modular Monolith

Monolith-First as a Rational Economic Equilibrium

📖 55 min read 🎯 Expert

Abstract

The modular monolith deliberately packages multiple bounded contexts inside a single deployable artifact while enforcing schema-per-module isolation and forbidden cross-module dependencies in CI. It is the rational response when deployment independence yields less economic utility than reduced network tax and operational surface area, consistent with Adaptive Granularity Governance: The Khan Microservice Pattern’s contraction phase (Chapter 11). We analyze failure modes of shared databases, connect to ArchUnit fitness functions, and quantify when extraction to microservices becomes dominant in the RVx calculus.

Service merge / consolidation - conceptual

Figure 18.1: Consolidating services when kinetic friction falls: merge economics dominate microservice isolation.


18.1 Problem formulation

Let modules (m \in \mathcal{M}) share a process. Coupling graph (G=(\mathcal{M},E)) edges denote compile-time imports. Goal: keep (G) acyclic at package level and ensure no ambient coupling through shared tables.

Definition 18.1 (Semantic monolith). A code arrangement that looks modular but shares mutable tables across contexts-distributed monolith precursor.


18.2 Schema-per-module on shared infrastructure

Use logical schemas (or separate databases later) with role-based grants prohibiting cross-schema joins for application roles. Read models cross boundaries only via integration events or explicit views owned by consuming modules.

Mechanism Guarantees
Schema grants Physical impossibility of accidental SQL joins
Module APIs Language-level encapsulation
Events Cross-context facts without shared tables

Schema-per-module - logical isolation on Aurora with ArchUnit-enforced module boundaries

Figure 18.2: Schema-per-module grants make cross-context SQL joins impossible for application roles.


Recipe 18.1: ArchUnit boundary enforcement (Java)

@AnalyzeClasses(packages = "com.example.commerce")
public class ModulithRules {
    @ArchTest
    static final ArchRule orders_do_not_import_inventory_internals =
        noClasses()
            .that().resideInAPackage("..orders..")
            .should().dependOnClassesThat()
            .resideInAPackage("..inventory.internal..");
}

Fail CI on violation: treat as compile-time blast shield.


18.3 Economic model (sketch)

Let (C_{\mathrm{net}}) be network + ops cost of extraction; let (C_{\mathrm{coup}}) coupling cost of staying merged. Extract when:

[ \mathbb{E}[\Delta \mathrm{RVx}] = f(C_{\mathrm{net}}, C_{\mathrm{coup}}) > \tau ]

(use project-specific calibration; Chapter 11 formulas).


18.4 Evolution to microservices

When extraction triggers, interfaces already exist at module seams: migrate with strangler routing (Chapter 19).


18.5 Limitations

Single runtime means single blast radius for catastrophic defects; mitigate with feature flags, traffic shadowing, and cell-aware deployments even inside monolith (logical cells).


18.6 Synthesis

The modular monolith is not an apology; it is disciplined modularity maximizing learning speed until evidence justifies network distribution.


Navigation: