Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 19

    Modular Monolith

    Modular monolith keeps a single deployable artifact while enforcing hard module boundaries inside the codebase — each module owns its schema slice, exposes a narrow public API,…

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Modular monolith keeps a single deployable artifact while enforcing hard module boundaries inside the codebase — each module owns its schema slice, exposes a narrow public API, and cannot import sibling internals. At Amazon scale, teams use it to defer microservice operational cost until domain boundaries and traffic patterns are proven.

    Real production story

    During Prime Day 2022, an internal ordering team at Amazon ran a single Spring Boot monolith where checkout, inventory, and pricing lived in packages with "soft" boundaries — any class could import any other. A pricing refactor renamed a DTO used by checkout; the deploy passed CI but broke tax calculation for 47 minutes because integration tests mocked cross-module calls.

    The post-incident ADR introduced a modular monolith: Gradle modules with enforced dependency rules (ArchUnit), each module owning its tables via schema-per-module inside one PostgreSQL instance, and inter-module communication only through published interfaces or domain events on an in-process event bus. Six months later the same team extracted inventory to a service in two weeks because the boundary already matched production ownership.

    Business problem

    Business pressure: Amazon's retail platform must ship weekly while sustaining 99.99% checkout availability. Microservices too early fragment ownership and inflate incident surface; a ball-of-mud monolith blocks parallel team velocity and makes every deploy a company-wide risk.

    • Revenue at risk: Undetected cross-module coupling caused a $12M estimated GMV impact during a single Prime Day deploy window.
    • Engineering velocity: 14 teams touching one repo without module fences averaged 3-day merge queues and correlated rollbacks.
    • Compliance / trust: PCI scope could not shrink until payment code was physically isolated in a module with separate audit trail.

    Architecture overview

    A modular monolith is one deployable unit composed of replaceable modules. Each module has: (1) a public API package, (2) private implementation, (3) owned persistence, (4) forbidden inward dependencies from siblings.

    • Definition: Single process, multiple bounded modules — not "folders by layer" but folders by subdomain.
    • When to adopt: 3+ teams, clear subdomain seams, need independent evolution without Kubernetes tax.
    • When to defer: Solo founder MVP — a plain monolith is faster; add modules when the second team arrives.
    • Operability: One dashboard, but per-module metrics (latency, error rate) tagged by module name in logs and traces.

    Architecture motivation

    Why architects care: Modular monolith delivers logical service boundaries with physical operational simplicity — one artifact, one pipeline, one runtime — while preserving the option to extract hot modules to services later.

    • Force: Domain boundaries are fuzzy; premature network splits create distributed monoliths worse than a well-fenced monolith.
    • Constraint: Cannot afford 40 microservice on-call rotations for a product still finding market fit.
    • Outcome: Enforced module APIs, schema ownership, and event seams that survive extraction without rewrite.

    Internal architecture

    Amazon retail modular monolith — modules as deployment-ready slices:

    • ArchUnit or Gradle dependency rules fail CI on illegal cross-module imports.
    • Each module publishes only com.amazon.order.checkout.api — never .internal.
    text
    ┌─────────────────────────────────┐
    │ Single deployable JAR/WAR │
    ┌──────────┐ │ ┌─────────┐ ┌─────────┐ ┌──────┐ │
    │ ALB │───────▶│ │Checkout │ │Inventory│ │Pricing│ │
    └──────────┘ │ │ module │ │ module │ │module │ │
    │ │ API ▲ │ │ API ▲ │ │ API ▲ │ │
    │ └───┬─────┘ └───┬─────┘ └──┬───┘ │
    │ │ events │ events │ │
    │ └────────────┴───────────┘ │
    │ In-process bus │
    └──────────────────┬──────────────────┘
    PostgreSQL (schema per module)
    checkout.* | inventory.* | pricing.*

    Data flow

    Primary path: HTTP hits checkout module facade; checkout validates cart via inventory module's public API (sync call or cached read model); pricing module computes totals; checkout persists order in checkout.orders and emits OrderPlaced via transactional outbox.

    • Write path: Single DB transaction within module; cross-module effects via outbox → message relay → in-process or Kafka handler.
    • Read path: Module serves its own read models; cross-module reads go through published query APIs, never direct SQL.
    • Async path: Domain events carry idempotency keys; inventory module decrements stock idempotently on OrderPlaced.

    System design diagram

    Two diagrams show the Modular Monolith topology and the primary request/event path used in production at scale.

    Modular Monolith — system view
    API Gateway
    Edge
    Checkout mod
    Core
    Inventory mod
    Data
    Pricing mod
    Async
    High-level topology for Modular Monolith.
    Modular Monolith — request / event flow
    HTTP ingress
    Ingress
    Module facade
    Store
    Owned schema
    Store
    Outbox event
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Gradle module structure with ArchUnit enforcement — pattern Amazon platform teams wire in CI:

    • Run ArchUnit on every PR — boundary violations are merge blockers, not suggestions.
    • Publish :module-api artifacts separately so extraction is a dependency change, not a rewrite.
    java
    // build.gradle.kts — module graph
    dependencies {
    constraints {
    api(project(":checkout-api"))
    implementation(project(":checkout-internal"))
    // inventory module visible ONLY via checkout-api
    }
    }
    // ArchUnit test — fails CI on boundary violation
    @AnalyzeClasses(packages = "com.amazon.order")
    class ModuleBoundaryTest {
    @ArchTest
    static final ArchRule checkoutMustNotImportInventoryInternal =
    noClasses().that().resideInAPackage("..checkout..")
    .should().dependOnClassesThat()
    .resideInAPackage("..inventory.internal..");
    @ArchTest
    static final ArchRule modulesTalkViaApiOnly =
    classes().that().resideInAPackage("..checkout..")
    .should().onlyDependOnClassesThat()
    .resideInAnyPackage("..checkout..", "..inventory.api..", "java..");
    }
    // Transactional outbox per module
    @Entity @Table(schema = "checkout", name = "outbox")
    class OutboxEvent {
    @Id UUID id;
    String aggregateType;
    String eventType;
    String payload;
    Instant createdAt;
    }

    Enterprise case study

    Amazon internal ordering platform (2022–2024): Migrated from layered monolith to modular monolith, then extracted inventory service when QPS exceeded 80k/min on stock checks.

    • Before: 23-minute mean rollback time; any team's bug could break checkout.
    • Decision: Gradle multi-module + ArchUnit + schema-per-module + outbox events.
    • After: Deploy frequency 3×; inventory extraction completed in 11 days with zero schema rewrite.

    Trade-offs

    • Simplicity vs isolation: One deploy means correlated failure — module bulkheads (thread pools, circuit breakers) mitigate but do not eliminate.
    • Schema-per-module vs one schema: Separate schemas enforce ownership; shared DB instance keeps ops simple and enables cross-module transactions when truly needed.
    • Extraction readiness vs YAGNI: Module seams cost upfront discipline; payback when traffic or team count forces service split.

    Security considerations

    Security is architectural: PCI module isolation reduces audit scope; payment card data never crosses into catalog module memory.

    • Identity: Internal module calls still authenticate via service identity tokens even in-process — prepares extraction.
    • Data: Row-level security per module schema; encryption keys scoped per module for at-rest data.
    • Supply chain: Single artifact means one SBOM scan path — simpler than 20 service images but blast radius is wider.

    Scalability analysis

    Scale dimensions: Amazon engineers scale the modular monolith vertically first, then extract the hottest module (usually checkout or inventory) horizontally as an independent service.

    • Horizontal scale: Stateless replicas behind ALB; session stickiness avoided via externalized cart state in ElastiCache.
    • Hot spots: Pricing module CPU spikes during promotions — isolate thread pool and consider async repricing pipeline.
    • Cost: One cluster vs 12 microservice clusters — modular monolith saves 60–70% infra until QPS per module exceeds single-fleet capacity.

    Failure scenarios

    What breaks: A memory leak in pricing module takes down the entire JVM — module boundaries are logical, not physical.

    • Runaway thread pool: Checkout calls inventory synchronously; inventory slowdown blocks all checkout threads — enforce timeouts and bulkheads per module.
    • Schema migration collision: Two modules migrate same DB instance concurrently — use module-scoped migration tools (Flyway schemas).
    • False extraction: Team extracts module but keeps synchronous HTTP between services — recreates distributed monolith with network latency.

    Staff engineer insights

    • If you cannot draw module boundaries on a whiteboard without mentioning HTTP or Kafka, they are not real boundaries yet.
    • Modular monolith is a stepping stone, not a destination — invest in extraction triggers (QPS, team count, deploy conflict rate) upfront.
    • Amazon's lesson: enforce boundaries in CI, not in code review comments — humans forget, ArchUnit does not.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionHow is a modular monolith different from a well-structured monolith?+

    Answer

    Structure alone is not enough — modular monolith enforces compile-time and CI-time boundaries (separate modules, forbidden imports, owned schemas). A "well-structured" monolith without enforcement decays in two quarters when deadlines pressure teams to take shortcuts.

    Follow-up

    What tooling would you use in a Node.js codebase?
    2AdvancedQuestionWhen would you extract a module to a microservice from a modular monolith?+

    Answer

    When independent scaling, deployment cadence, or failure isolation requirements exceed what bulkheads in a shared JVM can provide — typically when one module's QPS, memory, or release frequency dominates and hurts siblings. Measure deploy conflict rate and p99 per module first.

    Follow-up

    How do you avoid a distributed monolith after extraction?
    3AdvancedQuestionCan modules share a database transaction in a modular monolith?+

    Answer

    Technically yes if they share a DB instance and you accept coupling — but cross-module transactions violate the pattern's intent. Prefer saga or outbox for cross-module consistency; reserve shared transactions only for proven, stable seams with an ADR.

    Follow-up

    How does the outbox pattern work in-process?

    Architecture review questions

    • Does each module have a published API package and private implementation hidden from siblings?
    • Are cross-module dependencies enforced in CI (ArchUnit, dependency-cruiser, Gradle constraints)?
    • Does each module own its schema or table prefix with independent migrations?
    • Are cross-module side effects async via outbox/events, not direct table writes?
    • Are per-module metrics (latency, errors, saturation) tagged in logs and dashboards?
    • Is there a documented extraction trigger (QPS, team count, failure correlation) per module?

    Summary

    Modular monolith at Amazon scale means treating internal modules like microservices that happen to share a process — owned schemas, public APIs, event seams, and CI-enforced dependency rules. Master the pattern when you need team parallelism without Kubernetes operational tax, and know when extraction metrics say the shared JVM is exhausted.

    Ready to mark this lesson complete?Track your journey across the entire course.