Modular Monolith vs Microservices

Modular Monolith vs Microservices

Modular Monolith vs Microservices

A beginner-friendly, production-grade tutorial that explains modular monoliths from first principles — what they are, why experienced architects often recommend them before microservices, and how to build one the right way.

01 · Foundations

Introduction & History

Every backend team eventually faces the same fork in the road: build one big application, split into many small services, or find the disciplined middle ground. Here’s how the industry got here.

Imagine you are building a house. You could build it as one giant room with no walls — a kitchen table next to a bed next to a bathtub, everything jumbled together. Or you could build it with separate rooms — a kitchen, a bedroom, a bathroom — but all inside one house, sharing one roof, one front door, and one electricity meter. Or you could go even further and build five separate small houses on the same street, each with its own roof, its own door, its own electricity connection, and its own driveway.

Software architecture has the exact same three choices. The “one giant room” is called a Big Ball of Mud monolith. The “one house, many rooms” is called a modular monolith. The “five separate houses” is called microservices.

This tutorial answers one very practical, very common question that comes up in almost every architecture interview and every real engineering team today: if microservices are so popular, why would anyone choose to build a modular monolith instead?

The core analogy

A modular monolith is like a well-organised house with clearly separated rooms, each with its own purpose, but all sharing one foundation. Microservices are like separate houses on a street — more independent, but you now need roads, mailboxes, and traffic rules to connect them.

1.1 · A short history of how we got here

In the 1990s and early 2000s, almost every application was a monolith by default. There wasn’t really a “choice” being made — tools, frameworks, and deployment models simply assumed you built one large application, packaged it into one deployable unit (a WAR file for Java, a single Rails app, a single PHP codebase), and ran it on one or a few servers.

As companies like Amazon, Netflix, and Google grew to enormous scale in the 2000s and early 2010s, they discovered a real problem: their monoliths had become so large that thousands of engineers were stepping on each other’s toes inside the same codebase. A small change by one team could accidentally break a completely unrelated feature owned by another team. Deployments became slow, risky, all-or-nothing events. To solve this, these companies split their systems into many small, independently deployable services — and the term microservices was popularised around 2011–2014, largely through writing and talks by James Lewis and Martin Fowler.

The industry, watching Netflix and Amazon’s success, rushed to copy the pattern. Between roughly 2015 and 2020, “microservices-first” became the default advice given in blog posts, conference talks, and job interviews — even for small startups with five engineers and a handful of users. Many of these teams ran into serious trouble: they inherited all the operational complexity of a distributed system (network calls, service discovery, distributed data consistency) without ever having enough scale or enough engineers to justify it.

This overcorrection led to a second wave of thinking, roughly from 2018 onward, championed by writers like Simon Brown (who coined the term “modular monolith” explicitly around 2014–2015) and later reinforced by real-world case studies — most famously Shopify’s and Amazon Prime Video’s public accounts of consolidating services back into more monolithic structures. The modern, mature position that most senior architects hold today is: start with a well-modularized monolith, and extract microservices later, only when you have a concrete, measured reason to.

Evolution of Backend Architecture Thinking 1990s – 2000s Monoliths by default 2011 – 2014 Netflix & Amazon popularise microservices 2015 – 2020 "Microservices-first" industry hype 2015 – 2018 Simon Brown formalises the "Modular Monolith" 2018 – 2022 Premature-microservices failures documented 2023 → today "Modular first, split later" is mainstream Three decades of pendulum swings — landing on a balanced middle ground.
Fig 1 — How industry thinking about monoliths vs. microservices has shifted over three decades.

This tutorial will walk you through everything you need to understand this topic deeply — from the very basic definitions, through the internal mechanics, all the way to production concerns like scaling, security, and real company case studies — so that you can both build a modular monolith correctly and explain, with confidence, exactly why it is often the smarter starting point.

02 · Motivation

Problem & Motivation

To understand why the modular monolith exists, you first need to understand the two problems it sits between.

On one side is the pain of a messy, unstructured monolith. On the other side is the pain of adopting microservices too early. The modular monolith is the architecture that tries to avoid both pains at once.

2.1 · Problem 1: the “Big Ball of Mud” monolith

When a team builds a monolith without discipline, code from different features gets tangled together. The billing code calls directly into the user code, which calls directly into the shipping code, which reaches back into the billing code — with no clear boundaries anywhere. Over a few years, this becomes what architects call a Big Ball of Mud: a system so tangled that nobody fully understands it, and every change feels dangerous.

!
Why this happens

It’s not because monoliths are inherently bad. It’s because nothing in a plain monolith stops a developer from importing any class from any other part of the codebase. Without enforced boundaries, convenience always wins over discipline — until it doesn’t.

2.2 · Problem 2: jumping to microservices too early

Microservices solve real problems for organisations with hundreds of engineers working on the same product. But they come with an unavoidable “tax”: every service boundary is now a network boundary. A function call that used to take microseconds inside one process now becomes a network request that can fail, time out, or arrive out of order. You now need service discovery, distributed tracing, retry logic, circuit breakers, container orchestration, and a way to keep data consistent across multiple databases.

For a small team — say, 3 to 15 engineers — this tax is rarely worth paying on day one. The team ends up spending more time managing Kubernetes clusters, message queues, and network failures than actually building product features that customers want.

70%+
of early-stage microservice migrations report higher operational overhead than expected (industry surveys, 2019–2023)
1
deployable unit in a modular monolith vs. dozens in microservices
0 ms
network latency between modules communicating in-process
2015
year Simon Brown popularised “Modular Monolith” as a named pattern

2.3 · The motivation for a third option

The modular monolith exists because teams realised the real problem was never “monolith vs. microservices” as a binary choice. The real problem was always lack of clear boundaries. A monolith with clear internal boundaries solves the tangled-code problem. And because it stays a single deployable unit, it avoids the network-call tax of microservices — for as long as the team doesn’t actually need independent scaling or independent deployment of each piece.

The analogy

Think of a toy storage box for a child’s room. A messy monolith is a single box with every toy — Lego, crayons, dolls, cars — thrown in randomly. Microservices are ten separate boxes, one per toy type, each requiring you to walk to a different shelf. A modular monolith is one box with well-labelled compartments inside: still one box to carry, but everything has its place.

In short: the modular monolith is motivated by wanting the organisational clarity of microservices without paying the distributed systems tax before you actually need it.

03 · Vocabulary

Core Concepts

Before going further, let’s build a solid vocabulary. Every term below is explained the same way: what it is, why it exists, where it’s used, a simple analogy, and a small example.

3.1 · Monolith

What it is: A single application where all the code — user management, billing, inventory, notifications — is compiled and deployed together as one unit (one process, one deployable file, one running program).

Why it exists: It’s the simplest possible way to build software. One codebase, one build, one deploy, one place to look for bugs.

Where it’s used: Most startups, most internal business tools, and even many large-scale systems (Shopify’s core commerce platform is famously still a monolith).

Analogy

A monolith is one single suitcase you pack everything into for a trip — shirts, shoes, chargers, all in one bag.

Example: An online bookstore where the code that handles logins, book search, shopping cart, and payment all live in one Java project and get deployed as a single .jar file.

3.2 · Modular Monolith

What it is: A monolith that is deliberately organised into well-defined, loosely-coupled internal modules — each module owns its own code and, ideally, its own data — but all modules still run inside the same process and are deployed as a single unit.

Why it exists: To get the organisational clarity of separated concerns (like microservices give you) while keeping the operational simplicity of a single deployment (like a monolith gives you).

Where it’s used: Increasingly the default recommendation for new products — used by companies like Shopify (their core platform), Basecamp, and many mid-size SaaS companies.

Analogy

A modular monolith is a suitcase with separate zippered compartments — one for shirts, one for shoes, one for electronics. Still one suitcase, but nothing gets tangled together.

Example: The same bookstore, but now organised into a UserModule, a CatalogModule, a CartModule, and a PaymentModule — each in its own Java package, each exposing a clear public interface, and each forbidden (by build tooling) from directly touching another module’s internal classes.

3.3 · Microservices

What it is: An architecture style where an application is split into many small, independently deployable services, each running in its own process, usually communicating over the network (HTTP, gRPC, or messaging queues).

Why it exists: To let large organisations scale both their systems (deploy and scale each service independently) and their teams (each team owns and ships its own service without waiting on others).

Where it’s used: Netflix, Amazon, Uber, and other large-scale companies with hundreds or thousands of engineers.

Analogy

Microservices are like moving out of the one-suitcase-per-trip model entirely and shipping separate boxes ahead of you by courier — one box of clothes, one box of shoes, one box of electronics — each arriving independently, each needing its own tracking number.

3.4 · Module

What it is: A self-contained unit of code that groups together everything needed for one business capability (e.g., “billing”) and exposes only a small, deliberate public interface to the rest of the application.

Why it exists: To let developers reason about one part of the system without needing to understand the entire codebase.

Simple analogy: A module is like an organ in the human body — the heart has one job, and the rest of the body interacts with it through blood vessels (its “interface”), not by reaching directly into heart tissue.

3.5 · Bounded Context (from Domain-Driven Design)

What it is: A boundary within which a particular business concept has one, consistent meaning and one model. For example, the word “Customer” might mean something different to the Billing team (someone with a payment method) than to the Support team (someone with a ticket history). Each interpretation lives inside its own bounded context.

Why it exists: Large business domains are too complex for one single unified model. Splitting them into bounded contexts keeps each model simple and consistent within its own boundary.

Where it’s used: Bounded contexts are the standard way to decide where to draw module boundaries in a modular monolith, and later, where to draw service boundaries in microservices.

Analogy

Think of a country divided into states. Each state has its own local laws (its own “model” of how things work), but they all still belong to one nation (one deployable application, in the modular monolith’s case).

3.6 · Coupling and Cohesion

Coupling is how much one piece of code depends on the internal details of another piece of code. Cohesion is how strongly the responsibilities inside one piece of code belong together. Good architecture — whether monolith, modular monolith, or microservices — always aims for low coupling, high cohesion: modules that are internally focused and only loosely connected to each other.

Analogy

Cohesion is like a well-organised toolbox where all the screwdrivers are in one section. Coupling is like how tangled the toolbox’s internal wires are with the wires of the toolbox sitting next to it. You want tidy toolboxes (high cohesion) connected by short, simple cables, not tangled wires everywhere (low coupling).

3.7 · Module Boundary Enforcement

What it is: Technical mechanisms that stop one module from directly reaching into another module’s internals — for example, Java’s module system (JPMS), package-private classes, ArchUnit tests, or build-tool level separation (separate Gradle/Maven modules).

Why it exists: Good intentions alone don’t keep boundaries clean over time. Without enforcement, a modular monolith slowly decays back into a Big Ball of Mud.

Example: An ArchUnit test in a Java project that fails the build if any class in the catalog package imports a class from the billing.internal package.

3.8 · Comparison table — the three architectures at a glance

AspectPlain MonolithModular MonolithMicroservices
Deployable units11Many (one per service)
Internal boundariesWeak or noneStrong, enforcedStrong, enforced by process isolation
Inter-component callsDirect in-processIn-process, via interfacesNetwork (HTTP / gRPC / queue)
Data ownershipUsually shared, tangledPer-module schemas (same DB or separate)Per-service database
Independent scalingNoNoYes
Independent deploymentNoNoYes
Operational complexityLowLowHigh
Team autonomyLowMediumHigh
Best forTiny apps, prototypesMost startups & mid-size productsLarge orgs, high scale, many teams
04 · Anatomy

Architecture & Components

Let’s look at what actually sits inside a modular monolith. Using our earlier bookstore example, here is a typical structural layout.

A Modular Monolith — Inside One Deployable Application SINGLE DEPLOYABLE APPLICATION · ONE PROCESS User Module public: UserService internal: UserRepository PasswordHasher Catalog Module public: CatalogService internal: BookRepository SearchIndexer Cart Module public: CartService internal: CartRepository Payment Module public: PaymentService internal: GatewayClient Shared Kernel common types (Money, CustomerId), events Single Database, schema-per-module users.* · catalog.* · cart.* · billing.* Public APIs are the only doors. Internals stay hidden — enforced by tooling.
Fig 2 — A modular monolith’s internal architecture. Each module has a public API (its “front door”) and hides its internal classes. Modules talk to each other only through public interfaces — never by reaching into another module’s internals.

4.1 · The essential components

Module (Bounded Context)

A self-contained package like com.bookstore.billing, owning its own domain logic and data access.

Public API / Facade

The only door into a module — typically one or two interfaces like PaymentService that other modules are allowed to call.

Internal Package

Everything a module needs privately — repositories, helper classes — hidden from every other module using access modifiers or module systems.

Shared Kernel

A small, carefully-limited set of common types (e.g., Money, CustomerId) that every module is allowed to depend on, to avoid duplicated definitions.

In-Process Event Bus

A lightweight internal mechanism (e.g., Spring’s ApplicationEventPublisher) letting modules react to things happening elsewhere without directly calling each other.

Schema-per-Module Database

Even though it’s one physical database, each module gets its own schema / table set, and no module is allowed to query another module’s tables directly.

4.2 · Why the “public API only” rule matters so much

The single most important architectural rule in a modular monolith is this: a module may only be accessed through its public interface — never by directly instantiating or importing another module’s internal classes. This one rule is what separates a true modular monolith from a monolith that merely has folders named after features but is secretly tangled underneath.

!
Common misunderstanding

Simply organising code into folders like /billing, /users, /orders does NOT make something a modular monolith. If any class in /orders can still freely import and call any class inside /billing, you still have a Big Ball of Mud — it’s just wearing a disguise. Real modularity requires enforced boundaries, not just folder names.

4.3 · How boundaries are enforced in Java

There are three common, production-proven ways to enforce module boundaries in a Java codebase:

  • Multi-module Maven / Gradle builds: Each business module becomes its own build module (e.g., billing-module, orders-module). One module can only see another’s classes if it’s explicitly listed as a dependency and those classes are declared public.
  • Java Platform Module System (JPMS, since Java 9): Using module-info.java to declare exactly which packages are exported and which are hidden, enforced by the compiler and the runtime itself.
  • ArchUnit tests: Automated tests that run as part of the build pipeline and fail if a forbidden dependency is detected — for example, “no class outside billing.api may access billing.internal.”
Java — Enforcing a module boundary with ArchUnit
@AnalyzeClasses(packages = "com.bookstore")
class ModuleBoundaryTest {

    @ArchTest
    static final ArchRule billing_internals_should_not_be_accessed_from_outside =
        classes()
            .that().resideInAPackage("..billing.internal..")
            .should().onlyBeAccessed().byAnyPackage("..billing..");

    @ArchTest
    static final ArchRule modules_should_not_have_cyclic_dependencies =
        slices()
            .matching("com.bookstore.(*)..")
            .should().beFreeOfCycles();
}

This test runs on every build. If a developer on the Orders team accidentally writes new PasswordHasher() (a class that belongs inside the Billing module’s internals), the build fails immediately — long before the mistake reaches production.

05 · Internals

Internal Working

Now let’s look at exactly what happens, step by step, when modules inside a modular monolith need to talk to each other. There are three common communication patterns.

5.1 · Pattern 1 — direct interface call (synchronous, in-process)

Module A calls a method on Module B’s public interface directly. This is a normal Java method call — no network, no serialisation, no timeout handling needed. It executes in microseconds because it’s just a function call on the same call stack, inside the same process memory.

Java — Cart module calling Catalog module through its public interface
// Catalog module's public interface - this is the ONLY thing other modules can see
public interface CatalogService {
    BookDetails getBookDetails(BookId bookId);
    boolean isInStock(BookId bookId);
}

// Cart module - depends only on the interface, never on Catalog's internals
@Service
public class CartService {

    private final CatalogService catalogService; // injected interface, not concrete class
    private final CartRepository cartRepository;  // Cart module's OWN internal repository

    public CartService(CatalogService catalogService, CartRepository cartRepository) {
        this.catalogService = catalogService;
        this.cartRepository = cartRepository;
    }

    public void addToCart(CustomerId customerId, BookId bookId, int quantity) {
        if (!catalogService.isInStock(bookId)) {
            throw new BookOutOfStockException(bookId);
        }
        BookDetails details = catalogService.getBookDetails(bookId);
        Cart cart = cartRepository.findByCustomer(customerId);
        cart.addItem(bookId, details.price(), quantity);
        cartRepository.save(cart);
    }
}

5.2 · Pattern 2 — in-process events (asynchronous, decoupled)

Instead of calling another module directly, a module publishes an event describing something that happened. Other modules subscribe to events they care about, without the publishing module ever knowing who’s listening. This keeps modules even more loosely coupled.

Java — Order module publishes an event; Payment module reacts to it
// Order module publishes this event when an order is placed
public record OrderPlacedEvent(OrderId orderId, CustomerId customerId, Money totalAmount) {}

@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public void placeOrder(Order order) {
        orderRepository.save(order);
        eventPublisher.publishEvent(
            new OrderPlacedEvent(order.getId(), order.getCustomerId(), order.getTotal())
        );
    }
}

// Payment module listens - Order module has NO idea Payment module exists
@Component
public class PaymentEventListener {

    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        paymentService.chargeCustomer(event.customerId(), event.totalAmount());
    }
}
Analogy

Direct interface calls are like walking to your neighbour’s door and asking them a question face to face. Events are like putting a notice on a shared community board — you post it once, and anyone who’s interested reads it, without you needing to know who they are.

5.3 · Pattern 3 — shared kernel types

A tiny set of universally-needed types (like Money, CustomerId, or Address) live in a shared package that every module can depend on. This shared kernel must stay extremely small — if it grows too large, it becomes a hidden coupling point that undermines the whole point of modularity.

Checkout Flow — All In-Process, One Deployable App Customer Cart Module Catalog Module Order Module Payment Module addToCart(bookId, qty) isInStock(bookId) [in-process] true placeOrder() publish OrderPlacedEvent chargeCustomer() No network hops between modules — everything happens on one call stack.
Fig 3 — A checkout flow inside a modular monolith. Notice everything happens within one process — no network hops between modules, even though the modules are cleanly separated.
06 · Lifecycle

Data Flow & Lifecycle

Let’s trace a full request from the moment a customer clicks “Buy Now” to the moment they see an order confirmation, and understand each stage of its lifecycle inside a modular monolith.

  1. HTTP request arrives

    The single running application (one process, one port, e.g., 8080) accepts the incoming request.

  2. Controller layer receives it

    The Order module’s controller validates basic input — is the cart ID present? is the customer authenticated?

  3. Order module begins its business transaction

    It calls CartService.getCart() (Cart module’s public interface) — an in-process call, taking microseconds.

  4. Order aggregate is created

    The Order module builds an Order aggregate, calculates totals, and saves it via its own repository, inside its own database schema.

  5. An event is published

    The Order module publishes an OrderPlacedEvent. Because this is a modular monolith, the event is delivered synchronously within the same request thread by default (though it can be made asynchronous with a thread pool if needed) — there’s no message broker involved unless the team deliberately adds one.

  6. Payment module reacts

    It charges the customer through an external payment gateway (this is the one genuine network call in the whole flow — because payment gateways are third-party systems) and saves a payment record in its own schema.

  7. Notification module reacts too

    The same event triggers a confirmation email being queued.

  8. All of this can sit in one database transaction

    If designed that way, the whole business operation is atomic — if the payment fails, the order can be rolled back atomically, something that is much harder to achieve safely across multiple databases in a microservices setup.

  9. HTTP response returns to the customer

    Typically in well under 200 ms, because almost the entire flow was in-memory function calls.

i
Production insight

This ability to wrap an entire business operation in one ACID database transaction is one of the most underrated advantages of a modular monolith. In microservices, achieving the same guarantee requires patterns like the Saga pattern, compensating transactions, or the Outbox pattern — all of which add real engineering complexity.

6.1 · Request lifecycle diagram

Full "Place Order" Request Lifecycle HTTP Request Order Controller Validate input Begin transaction Cart.getCart() Create Order aggregate Save Order (own schema) Publish OrderPlacedEvent Payment reacts Notification reacts External Payment Gateway the ONLY real network call Commit / Rollback HTTP 200 / 402 Almost all the work is in-memory function calls — measured in microseconds.
Fig 4 — The full lifecycle of a “place order” request. Only one true network call exists (to the external payment gateway) — everything else stays in-process.
07 · Trade-offs

Advantages, Disadvantages & Trade-offs

No architecture is free of trade-offs. Here is an honest, balanced look at what you gain and what you give up by choosing a modular monolith.

Advantages

  • Single deployment — one build, one release pipeline, one thing to monitor.
  • No network latency between modules — calls are in-process function calls.
  • Strong transactional consistency using ordinary ACID database transactions.
  • Simpler local development — one application to run, debug, and step through.
  • Lower operational cost — no service mesh, no per-service infrastructure.
  • Refactoring module boundaries is easier before they become network boundaries.
  • Easier to trace bugs — one stack trace instead of distributed traces across services.
  • Clear team ownership of modules without full infrastructure overhead.

Disadvantages

  • Cannot scale individual modules independently (must scale the whole app).
  • One bug in one module can still crash the entire process if boundaries leak.
  • Requires strong engineering discipline to keep boundaries enforced over time.
  • All modules must typically use the same language / runtime (usually).
  • A single large build can become slower to compile and test as the app grows.
  • Deployment of any change requires redeploying the entire application.
  • Harder to give completely independent tech stacks to different teams.

7.1 · Trade-off framework — what are you actually trading?

You gainYou give up
Simplicity of a single deployable unitIndependent deployability of each module
Fast, reliable in-process communicationIndependent runtime scaling per module
Easy ACID transactions across modulesFull data isolation per module (unless you also split databases)
Lower infrastructure and operational costPolyglot flexibility (different languages per module)
Faster local development loopTeam-level deployment autonomy
Analogy

Choosing a modular monolith is like choosing to live in a well-designed apartment with separate rooms instead of immediately buying five separate houses across town. You get organisation without the overhead of five mortgages, five commutes, and five sets of keys — but you also can’t rent out just the kitchen independently.

08 · Scale

Performance & Scalability

A very common myth is that “monoliths don’t scale.” This is misleading. Let’s separate the truth from the myth.

8.1 · How a modular monolith actually scales

A modular monolith scales the same way a plain monolith does: horizontally, by running multiple identical copies (instances) of the entire application behind a load balancer. Since the application is stateless (session state stored externally, e.g., in Redis), any instance can handle any request.

Horizontal Scaling — Same App, More Copies Load Balancer App Instance 1 all modules App Instance 2 all modules App Instance 3 all modules Shared Database Redis Cache
Fig 5 — Horizontal scaling of a modular monolith — every instance runs the full application; the load balancer distributes traffic evenly.

8.2 · What you lose — independent scaling

The real limitation is this: if only the Catalog module (say, product search) is under heavy load while Billing is idle, you still have to scale the entire application, including Billing, because they’re bundled into one deployable unit. In microservices, you could scale just the Catalog service to 20 instances while Billing stays at 2.

!
When this actually matters

For the vast majority of applications, this “waste” is small and cheap — modern cloud compute is inexpensive, and running a few extra idle instances of the whole app costs far less than the engineering time required to build and operate independent microservices. This only becomes a real problem at genuinely large scale — think millions of requests per second with wildly different load patterns per feature.

8.3 · Performance is often actually better, not worse

Because inter-module calls stay in-process (nanoseconds to microseconds) instead of going over the network (milliseconds, plus serialisation overhead), a well-built modular monolith frequently has lower end-to-end latency than an equivalent microservices system for the same business operation, especially for workflows that touch many modules per request (like our checkout example).

~0.001 ms
typical in-process method call latency
1–50 ms
typical network call latency between microservices (same data centre)
5–10×
common latency multiplier for a workflow spanning 5+ microservices vs. one modular monolith

8.4 · Caching and read scalability

A modular monolith can use the same caching strategies as microservices: an in-memory cache (Caffeine) for hot, single-instance data, and a distributed cache (Redis) shared across instances for data that must stay consistent across the fleet. Because all modules share one process, cache invalidation logic can sometimes be simpler — a module can directly evict its own cache entries the moment it writes new data, without needing a distributed cache-invalidation event pipeline.

09 · Availability

High Availability & Reliability

Machines fail, threads block, disks fill up. Here’s how a modular monolith stays up regardless.

9.1 · The “blast radius” concern

The most legitimate reliability criticism of monoliths — modular or not — is blast radius: if one module has a severe bug (say, a memory leak or an infinite loop) that crashes the process, every module goes down with it, even ones that had nothing to do with the bug. In a true microservices architecture, a crash in the Recommendations service does not take down Checkout.

!
Real-world example

If a poorly-written report-generation feature inside a modular monolith runs an unbounded query that exhausts database connections, it can starve the connection pool for the entire application — including critical paths like checkout — even though report generation and checkout are “different modules.”

9.2 · How production teams reduce blast radius

  • Bulkheads: Give each module its own dedicated thread pool and, where possible, its own database connection pool, so one module exhausting resources doesn’t starve another.
  • Circuit breakers on risky internal calls: Even in-process, wrapping calls to a historically unstable module (e.g., one that makes external calls) with a circuit breaker (Resilience4j) prevents cascading failures.
  • Timeouts everywhere external systems are touched: Any module calling a third-party API (payment gateway, email provider) must use aggressive timeouts so a slow external dependency can’t block the whole application’s thread pool.
  • Careful use of async processing: Non-critical work (sending confirmation emails, generating reports) should run on background threads or queues, not block the main request path.

9.3 · Availability through redundancy, not isolation

Modular monoliths achieve high availability the same way any stateless application does: running multiple instances across multiple availability zones, with health checks and automatic instance replacement. A crash in one instance doesn’t take down the whole system — the load balancer simply routes around the failed instance while it’s replaced.

High Availability via Multi-AZ Redundancy Load Balancer + health checks AVAILABILITY ZONE A App Instance 1 AVAILABILITY ZONE B App Instance 2 AVAILABILITY ZONE C App Instance 3 Primary DB Read Replica
Fig 6 — High availability for a modular monolith through multi-AZ redundancy — not architectural fragmentation.

9.4 · Disaster recovery and backups

Because a modular monolith usually uses a single database (with per-module schemas), disaster recovery is often simpler than in microservices: one consistent backup and restore point instead of needing to coordinate consistent backups across many independent service databases. Standard practices apply: automated daily snapshots, point-in-time recovery (PITR) enabled, and regular restore drills to verify backups actually work.

10 · Protection

Security

Fewer moving parts means fewer places to defend — but also fewer natural boundaries, so discipline matters.

10.1 · Authentication and authorisation

In a modular monolith, authentication typically happens once, at the edge (a filter or interceptor), producing a verified identity (e.g., a JWT claim or session) that is passed down through the call chain in-process. This is simpler than microservices, where every service must independently validate tokens or trust a shared identity provider.

Java — Module-level authorisation check using Spring Security
@Service
public class PaymentService {

    @PreAuthorize("hasRole('CUSTOMER') and #customerId == authentication.principal.customerId")
    public PaymentResult chargeCustomer(CustomerId customerId, Money amount) {
        // authorization is enforced before this method body ever runs
        return gatewayClient.charge(customerId, amount);
    }
}

10.2 · Module-to-module trust

Because modules run in the same process and the same trust boundary, you generally do not need to re-authenticate or re-authorise calls between modules — the process boundary itself is the trust boundary. This is a meaningful simplification compared to microservices, where every service-to-service call typically needs mutual TLS (mTLS) or service-to-service tokens to prevent one compromised service from impersonating another.

!
The trade-off

This simplicity comes with a real risk: because there’s no network boundary between modules, a vulnerability in one module (e.g., a SQL injection in the Reviews module) has an easier path to reach sensitive data in another module (e.g., Payment) if the code boundary is ever violated. Enforced module boundaries aren’t just about clean code — they’re a security control too.

10.3 · Practical security checklist for a modular monolith

  • Validate and sanitise all input at the edge (controller layer) before it reaches any module.
  • Use parameterised queries / an ORM (Hibernate / JPA) everywhere to prevent SQL injection.
  • Keep secrets (API keys, DB passwords) in a secrets manager (AWS Secrets Manager, HashiCorp Vault), never in code.
  • Apply the principle of least privilege even between modules — a module’s public interface should expose only what’s needed, nothing more.
  • Encrypt sensitive data at rest (e.g., payment details) and in transit (TLS for all external traffic).
  • Run dependency vulnerability scans (OWASP Dependency-Check, Snyk) as part of the CI pipeline.
  • Apply rate limiting and input validation at the API gateway / edge layer to blunt abuse before it reaches any module.
Analogy

Security in a modular monolith is like security inside a single office building with separate departments on separate floors. Once you’re past the front-door security guard (authentication), you can walk between floors relatively freely (in-process trust) — which is convenient, but it also means a break-in on one floor is more dangerous than in a system of separate, individually-guarded buildings (microservices with network-level isolation).

11 · Observability

Monitoring, Logging & Metrics

One of the quiet, under-discussed benefits of a modular monolith is how much simpler observability is compared to microservices.

11.1 · Logging

Every log line from every module lands in a single log stream per instance. Debugging a bug that spans Cart, Order, and Payment means reading one continuous, chronologically-ordered log — no need to stitch together logs from five different services using a correlation ID (though it’s still good practice to include one).

Java — Structured logging with a correlation ID (still good practice even in a monolith)
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String correlationId = UUID.randomUUID().toString();
        MDC.put("correlationId", correlationId);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.remove("correlationId");
        }
    }
}

11.2 · Metrics

Per-module metrics (request counts, error rates, latency per module) can still be tracked separately using labeled metrics, even though everything runs in one process — for example, using Micrometer with a module tag on each metric, then visualising them separately in Grafana even though they share infrastructure.

11.3 · Distributed tracing — mostly unnecessary

Because most calls stay in-process, you generally don’t need a full distributed tracing system (like Jaeger or Zipkin) just to understand a request’s path within the monolith — a single stack trace already shows you the entire call chain. You still want tracing for the genuine external calls (payment gateway, third-party APIs), but the scope is much smaller than in microservices, where every single service hop needs a trace span to reconstruct the full picture.

Observability — Simpler Than Microservices Single App Instance one process, all modules Structured Logs one stream Metrics tagged by module APM Datadog / New Relic Log Aggregator ELK / CloudWatch Grafana Dashboards Alerting PagerDuty / OpsGenie
Fig 7 — A typical observability stack for a modular monolith — simpler than microservices because there’s only one process to instrument.

11.4 · Alerting

Standard production alerting practices apply: alert on symptoms (elevated error rate, high p99 latency, low throughput) rather than causes, use SLOs (Service Level Objectives) to define what “healthy” means, and route alerts through an on-call tool. Because there’s only one deployable unit, on-call rotations are simpler too — there’s one dashboard to check first, not fifteen.

12 · Operations

Deployment & Cloud

Fewer artefacts, fewer pipelines, fewer things to break during a release.

12.1 · What deployment looks like

A modular monolith is packaged as one artefact — for Java, typically a single executable JAR (using Spring Boot’s embedded server) — and containerised into one Docker image. Deployment means: build the image, push it to a registry, and roll it out.

Dockerfile — Packaging a Java modular monolith
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/bookstore-app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

12.2 · Deployment strategies that still apply

Even without microservices, modular monoliths benefit from modern, safe deployment strategies:

  • Rolling deployment: Replace instances one at a time behind the load balancer, so the app stays available throughout the deploy.
  • Blue-green deployment: Stand up an entirely new fleet (“green”) alongside the old one (“blue”), switch traffic over once green is verified healthy, then tear down blue.
  • Canary deployment: Route a small percentage of traffic (e.g., 5%) to the new version first, watch error rates and latency, then gradually increase.
CI/CD Pipeline — One Artifact, One Pipeline git push CI: build + test ArchUnit checks Docker image Push to registry Canary 5% Roll out 100% Automatic rollback healthy?
Fig 8 — A typical CI/CD pipeline for a modular monolith — one pipeline, one artefact, one set of deployment gates.

12.3 · Cloud infrastructure

A modular monolith typically runs on far simpler infrastructure than microservices: an autoscaling group of container instances (AWS ECS / Fargate, or a handful of Kubernetes pods) behind a load balancer, one relational database (often with read replicas), and a caching layer. There’s no need for a service mesh (Istio / Linkerd), no need for service discovery infrastructure (Consul / Eureka), and far fewer moving parts to secure, patch, and pay for.

$
Production insight · cost

Teams moving from premature microservices back to a modular monolith (Amazon Prime Video’s widely-discussed 2023 case study is the most cited example) have reported infrastructure cost reductions of 90% or more for the specific workflows they consolidated — largely from eliminating inter-service data transfer costs and reducing the number of always-on compute units needed.

12.4 · CI/CD pipeline speed

One deployable unit means one CI/CD pipeline to maintain, though as the codebase grows, build and test times can increase. This is manageable with good practices: parallelised test suites, build caching (Gradle build cache), and modular test suites where only tests relevant to changed modules run on every commit, with full suites running before merge to the main branch.

13 · Data

Databases, Caching & Load Balancing

The data layer is where a modular monolith earns most of its simplicity — and where a bit of discipline gives you microservice-grade data ownership without the infrastructure bill.

13.1 · Schema-per-module — the recommended data pattern

The best-practice approach in a modular monolith is one physical database, but a separate schema per module (e.g., billing.payments, catalog.books as distinct PostgreSQL schemas). Each module’s repository code may only query tables within its own schema. This gives you almost all of the data-ownership discipline of microservices, without the operational cost of running many separate database instances.

SQL — Schema-per-module layout in PostgreSQL
CREATE SCHEMA users;
CREATE SCHEMA catalog;
CREATE SCHEMA cart;
CREATE SCHEMA billing;

CREATE TABLE users.accounts (id UUID PRIMARY KEY, email TEXT UNIQUE NOT NULL);
CREATE TABLE catalog.books (id UUID PRIMARY KEY, title TEXT NOT NULL, price_cents INT);
CREATE TABLE billing.payments (id UUID PRIMARY KEY, order_id UUID, amount_cents INT);

-- The billing module's repository is only ever granted access to the billing schema
GRANT USAGE ON SCHEMA billing TO billing_service_role;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA billing TO billing_service_role;

Database-level roles and grants (as shown above) can enforce this even at the SQL level, so even a bug that bypasses application-layer boundaries still can’t touch another module’s tables.

13.2 · Caching strategy

Cache typeUse caseExample
In-memory (local)Very hot, small, per-instance data (e.g., feature flags)Caffeine cache inside the JVM
Distributed cacheData shared across all instances (e.g., product catalog, session data)Redis / ElastiCache
CDN / edge cacheStatic assets and cacheable public responsesCloudFront, Cloudflare

13.3 · Load balancing

Since every instance of a modular monolith is identical (all modules present in every instance), load balancing is straightforward: a simple round-robin or least-connections algorithm across all healthy instances works well. There’s no need for the more complex per-service routing rules that microservices architectures often require at the API gateway layer.

Data & Caching Layout in Production Client CDN / Edge Load Balancer round-robin Instance 1 Instance 2 Instance 3 Redis (shared cache) Postgres Primary Read Replica
Fig 9 — A typical data and caching layout for a modular monolith in production.

13.4 · Read replicas and scaling reads

Read-heavy modules (like Catalog, handling product search) can route their read queries to a read replica, while write-heavy modules (like Order) continue writing to the primary. This is a well-understood, mature pattern that requires no distributed systems complexity beyond what a well-run relational database already provides.

14 · Evolution

APIs & Microservices: The Migration Path

A well-built modular monolith isn’t a dead end — it’s often a deliberate stepping stone.

Because modules already have clean, enforced boundaries and public interfaces, extracting any one of them into a real microservice later becomes a mechanical, low-risk exercise rather than a painful untangling project.

14.1 · Why extraction is easy from a modular monolith

Since a module like PaymentService already only exposes a defined interface and never leaks internal details, converting it into a microservice mostly means: (1) wrap the same interface with a REST or gRPC controller, (2) replace the in-process call from other modules with an HTTP / gRPC client, and (3) move the module’s schema into its own physical database. The business logic inside barely changes.

Extracting a Module into a Microservice — Later BEFORE · IN-PROCESS CALL Order Module same process Payment Module same process method call AFTER · EXTRACTED MICROSERVICE Order Module app A Payment Service app B HTTP / gRPC Payment’s DB Same public interface. Only the transport and data ownership change.
Fig 10 — Extracting a module into a microservice later mostly swaps the transport layer — the public interface and business logic remain nearly identical.

14.2 · When it actually makes sense to extract a module

  • A module has genuinely different scaling needs — e.g., a recommendation engine needing GPU instances while the rest of the app needs only standard compute.
  • A module needs a different technology — e.g., a machine-learning-heavy module that’s much easier to build in Python while the rest of the system is Java.
  • A team has grown large enough that deploying the whole monolith on every change from every team is genuinely slowing everyone down (commonly cited as somewhere past 50–100 engineers on one codebase, though this varies).
  • Regulatory or compliance needs require physically isolating certain data (e.g., PCI-DSS scope reduction for payment data).
!
What NOT to do

Don’t extract a module into a microservice “because microservices are the modern way.” Extraction should always be driven by a concrete, measurable pain point — a specific scaling bottleneck, a specific team-velocity problem, or a specific compliance requirement — not by architectural fashion.

14.3 · API design inside the monolith (public HTTP surface)

Even though modules talk to each other in-process, the application as a whole still exposes a normal external API to clients — typically REST (with OpenAPI / Swagger documentation) or GraphQL. Good API design principles apply regardless of what’s happening internally: versioning (/api/v1/…), consistent error formats, pagination for list endpoints, and idempotency keys for operations like payments.

Java — A REST controller exposing the Order module’s external API
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<OrderResponse> placeOrder(
            @RequestHeader("Idempotency-Key") String idempotencyKey,
            @Valid @RequestBody PlaceOrderRequest request) {
        Order order = orderService.placeOrder(request.toCommand(), idempotencyKey);
        return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(order));
    }
}

14.4 · The “modulith to microservices” spectrum

It’s useful to think of this not as two options but a spectrum: plain monolith → modular monolith → modular monolith with separate module databases (sometimes called a “distributed monolith” if done carelessly, or a legitimate hybrid if done well) → selectively extracted microservices for high-value modules → full microservices. Mature organisations move along this spectrum gradually, driven by evidence, not by starting at the far end on day one.

15 · Patterns

Design Patterns & Anti-patterns

The patterns that keep a modular monolith healthy — and the anti-patterns that quietly turn it back into a Big Ball of Mud.

15.1 · Helpful design patterns

Facade Pattern

Each module exposes one clean interface (facade) hiding its internal complexity — the foundation of the “public API only” rule.

Repository Pattern

Each module accesses its own data through a repository interface, keeping persistence details out of business logic and out of other modules’ reach.

Domain Events

Modules communicate side effects (e.g., “order placed”) via events instead of direct calls, reducing coupling between modules.

Anti-Corruption Layer

A thin translation layer at a module’s boundary that converts external / legacy data shapes into the module’s own clean domain model.

Hexagonal / Ports & Adapters

Each module’s core business logic depends only on interfaces (ports); concrete implementations (database, external APIs) are swappable adapters.

CQRS (within a module)

Separating read models from write models inside a busy module (e.g., Catalog) to optimise search-heavy reads independently of writes.

15.2 · Anti-patterns to actively avoid

The “Fake Modularity” anti-pattern

  • Folders named after features, but classes freely import across folder boundaries.
  • No automated test (like ArchUnit) enforcing the boundary.
  • Result: looks organised, behaves exactly like a Big Ball of Mud.

The “Shared Database Table” anti-pattern

  • Multiple modules directly reading and writing the same table.
  • No single module owns the data’s lifecycle or validation rules.
  • Result: hidden coupling that’s invisible in the code but very real in the database.

The “Distributed Monolith” anti-pattern

  • Splitting into microservices too early, but services still share a database or deploy in lockstep.
  • Gets all the network latency and operational cost of microservices, with none of the independence benefits.
  • Widely considered the worst possible outcome of a botched migration.

The “God Module” anti-pattern

  • One module (often “Core” or “Common”) keeps absorbing responsibilities until it depends on, and is depended on by, everything else.
  • Defeats the entire purpose of modularity — everything becomes coupled through the God Module.
Analogy for the Distributed Monolith anti-pattern

This is like splitting one house into five separate buildings, but leaving them all connected to the same single water pipe and the same single electricity meter. You now have the inconvenience of walking between buildings (network calls) without gaining any real independence (a burst pipe still takes down every building).

16 · Discipline

Best Practices & Common Mistakes

A short, opinionated checklist of habits that keep modular monoliths healthy over the long haul — and the mistakes teams keep repeating.

16.1 · Best practices

  • Design module boundaries around business capabilities (bounded contexts), not technical layers — “Billing” and “Catalog” are good module names; “Controllers” and “Repositories” are not.
  • Enforce boundaries with automated tests from day one — ArchUnit or module-system checks in CI, not just team discipline.
  • Keep the shared kernel tiny — a handful of universal types at most; resist the urge to put business logic there.
  • Give each module its own database schema even if they share a physical database instance.
  • Prefer events over direct calls for anything that isn’t strictly required to happen synchronously within the same transaction.
  • Document module ownership — which team owns which module — even before you have separate deployments, so the social boundary matches the code boundary.
  • Write integration tests per module so each module’s correctness can be verified somewhat independently of the others.
  • Revisit boundaries periodically — the right module boundaries often only become clear after building the real system for a while; be willing to refactor them.

16.2 · Common mistakes teams make

MistakeWhy it hurtsFix
No enforced boundaries, just folder namesDecays into a Big Ball of Mud within monthsAdd ArchUnit / module-system checks in CI
One shared “utils” or “common” package that keeps growingBecomes a God Module coupling everything togetherKeep shared code minimal; duplicate small things instead of over-sharing
Modules directly querying each other’s database tablesHidden coupling invisible in code reviewSchema-per-module with DB-level access grants
Treating module boundaries as fixed foreverWrong initial boundaries calcify and become expensive to fixReview and refactor boundaries as understanding of the domain improves
Jumping straight to microservices “to be safe for the future”Pays distributed-systems tax before it’s neededStart modular monolith; extract only with concrete evidence
No ownership clarity over modulesNobody feels responsible for a module’s qualityAssign clear team / individual ownership per module, mirrored in code review rules
i
A useful rule of thumb

Many experienced architects suggest a simple heuristic: “You are almost never wrong to start with a modular monolith. You are very often wrong to start with microservices.” The cost of staying modular-but-monolithic slightly too long is usually low. The cost of adopting microservices too early is usually high and hard to reverse.

17 · In production

Real-World & Industry Examples

The theory is only as good as the companies actually running it. Here are the most-cited case studies on both sides of the modular-vs-microservices debate.

17.1 · Shopify — the most-cited modular monolith at scale

Shopify runs one of the largest e-commerce platforms in the world on what it calls a “Majestic Monolith” — a single deployable Ruby on Rails application internally organised into modular components with enforced boundaries. This single deployable application is how Shopify handles billions of dollars in sales during events like Black Friday, and its codebase has grown to over 2.8 million lines of Ruby code, yet is deployed multiple times a day by hundreds of developers.

Shopify didn’t start this way by accident — the company deliberately evolved its architecture into a modular monolith, keeping all code in one codebase while ensuring boundaries were clearly defined and respected between different components. Internally, the monolith’s structure consists of multiple components each focused on a specific business domain, supported by custom tooling built to enforce coding standards and API boundaries between them. Shopify also shards its databases by shop using an independent-cluster “pods” approach, isolating tenants from each other and containing the impact of any single noisy customer, while keeping stateless application layers scaling automatically via Kubernetes.

i
Key takeaway from Shopify

Modularity and massive scale are not opposites. Shopify proves that a disciplined modular monolith can handle enterprise-grade, high-traffic e-commerce workloads without needing to fragment into hundreds of microservices.

17.2 · Amazon Prime Video — the cautionary tale about premature microservices

In 2023, Amazon Prime Video’s Video Quality Analysis team published a widely-discussed account of moving one of their services in the opposite direction — from a distributed, serverless microservices setup back to a more monolithic design. Their original architecture split video processing into separate microservices, including a frame-splitting service that temporarily uploaded images to S3 and separate defect-detector microservices that downloaded and processed them concurrently — but the high volume of calls to S3 turned out to be expensive. After consolidating the pipeline into a single process, the team reported that moving to a monolith cut their infrastructure cost by over 90%, while also increasing their scaling capability to handle thousands of streams with room to grow further.

!
Important nuance

It’s worth being precise about this case study: Prime Video overall did not abandon microservices — the broader Prime Video platform is still built from many services; only this one specific team refactored their component into a more monolithic design because it was better suited to their workload. The lesson isn’t “microservices are bad,” it’s “match the architecture to the actual problem, and don’t assume network-call-per-step is free.”

17.3 · Basecamp and GitHub

Basecamp, whose founders (David Heinemeier Hansson and Jason Fried) have written extensively in favour of what they call the “majestic monolith,” has run its entire product on a single Ruby on Rails codebase for its full history — deliberately avoiding microservices even at meaningful scale. GitHub similarly uses modular monoliths to manage core functionality like repositories and pull requests, favouring this approach to maintain team agility while avoiding the operational complexity that a fully distributed microservices architecture would introduce.

17.4 · Netflix and Uber — when microservices genuinely paid off

It’s equally important to recognise the other side. Netflix and Uber operate at a scale — thousands of engineers, hundreds of millions of users, and highly variable load across very different product areas — where microservices deliver real, measurable benefits: independent scaling of wildly different workloads (video encoding vs. billing vs. recommendations), and the ability for hundreds of independent teams to ship without blocking each other. These companies didn’t start with microservices either — they grew into them as concrete scaling and organisational pain points emerged.

17.5 · Summary of what the industry examples teach us

CompanyChoiceWhy it worked for them
ShopifyModular monolith at massive scaleStrong internal discipline + database sharding gave them scale without service fragmentation
Amazon Prime Video (VQA team)Moved one service from microservices back to monolithNetwork / data-transfer overhead between services was the actual bottleneck, not compute
BasecampMonolith by philosophySmall, focused team; simplicity valued over independent scaling
Netflix, UberFull microservicesGenuinely massive scale, huge engineering orgs, highly divergent workloads per team
18 · Recap

FAQ, Summary & Key Takeaways

The questions that come up in every architecture review — answered plainly — followed by a compact takeaways checklist.

18.1 · Frequently Asked Questions

Is a modular monolith the same as a “normal” monolith?

No. A normal monolith has no enforced internal boundaries — any code can call any other code. A modular monolith deliberately organises code into modules with enforced public interfaces, even though it’s still deployed as one unit.

Can a modular monolith scale to millions of users?

Yes. Scaling is horizontal — running many identical instances behind a load balancer — the same fundamental technique microservices use for each individual service. Shopify’s billions of dollars in Black Friday sales on a modular monolith is direct proof this works at serious scale.

Do I need Kubernetes for a modular monolith?

Not necessarily. Many modular monoliths run happily on simpler infrastructure like AWS ECS / Fargate, a managed platform (Heroku, Render, Fly.io), or a small set of VMs behind a load balancer. Kubernetes becomes more valuable once you have many independently deployable services to orchestrate.

When should I actually move to microservices?

When you have concrete, measured evidence of one of these: a specific module needs independent scaling due to genuinely different load patterns, a specific module needs a different technology stack, your engineering organisation has grown large enough that a single shared deployment pipeline is measurably slowing multiple teams down, or you have a hard compliance requirement for physical data isolation.

Is it hard to migrate from a modular monolith to microservices later?

If the modular monolith was built with genuinely enforced boundaries and clean public interfaces, extracting a module into a microservice is a comparatively mechanical exercise — mostly swapping in-process calls for network calls. It’s migrating from a tangled, unmodularized monolith that is genuinely hard.

What programming languages / frameworks support modular monoliths well?

Any mainstream stack works: Java (Spring Modulith, multi-module Maven / Gradle, JPMS), Ruby on Rails (Packwerk, used by Shopify), .NET (Areas / Modules with Clean Architecture), Node.js (Nx monorepo with enforced module boundaries), and Python (well-structured Django apps with clear app boundaries).

18.2 · Summary

A modular monolith is a single deployable application built from well-defined, loosely-coupled internal modules, each exposing only a clean public interface and hiding its own internal implementation and data. It sits deliberately between two extremes: a tangled, boundary-free monolith (the Big Ball of Mud) on one side, and a fully distributed microservices architecture on the other.

Teams choose a modular monolith over jumping straight to microservices because it delivers most of the organisational and architectural clarity that makes microservices attractive — clear ownership, low coupling, high cohesion — while avoiding the real costs that come with network boundaries: latency, partial failure handling, distributed transactions, service discovery, and significantly higher operational overhead. For the vast majority of teams — especially those under roughly 50–100 engineers or without a proven, specific scaling bottleneck — this trade-off strongly favours starting modular-monolithic and evolving toward microservices deliberately, only when real evidence demands it.

18.3 · Key takeaways

  • A modular monolith enforces internal module boundaries but stays a single deployable unit — you get organisation without network overhead.
  • The core discipline is: modules communicate only through public interfaces, never by reaching into each other’s internals — and this must be enforced by tooling (ArchUnit, JPMS, multi-module builds), not just convention.
  • Schema-per-module (even within one physical database) gives you most of microservices’ data-ownership discipline without running many separate databases.
  • In-process communication (direct calls and in-process events) is dramatically faster and simpler than network calls, and lets you keep strong ACID transactional guarantees across what would otherwise be separate services.
  • The main real trade-off you give up is independent scaling and independent deployment per module — which matters far less than most teams assume until they hit genuine, measured scale.
  • Real companies — Shopify, Basecamp, GitHub — run enormous, profitable, high-traffic systems on modular monoliths, while Amazon Prime Video’s own case study shows the real cost that premature microservices fragmentation can introduce.
  • A well-built modular monolith is not a dead end — its enforced boundaries make future extraction into microservices a low-risk, mechanical process, driven by evidence rather than fashion.
Think back to the house analogy from the start. A modular monolith is a well-designed house with clear, separate rooms sharing one strong foundation. You can always build an extension, or eventually move a room into its own separate building later, once you know for certain you need to. But you never have to start by building five separate houses just to keep your kitchen away from your bedroom — a good set of internal walls does that job perfectly well, for a lot longer than most people expect.
#modular-monolith #microservices #monolith #software-architecture #ddd #bounded-context #archunit #spring-modulith #shopify #prime-video #basecamp #system-design