SOFTWARE ARCHITECTURE / SYSTEM CONCEPT BRIEF

Modular monolith

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.

IntermediatePhase 08 / Topic 2 of 17RequirementsTrade-offsFailure modes
01

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.

An office building with departments

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.

02

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.
03

Where it shows up in interviews

Evolutionary architecture

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
04

Where it is used in real software

Shopify components

Shopify split its Rails monolith into components with enforced boundaries using its Packwerk tool.

Spring Modulith

A Spring project for building modular monoliths with verified module boundaries and events.

Java modules and ArchUnit

JPMS and ArchUnit tests prevent illegal dependencies between packages.

05

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.
06

How it works, step by step

  1. 1
    Identify bounded contexts

    Catalog, ordering, payments, shipping.

  2. 2
    Create modules with public APIs

    Hide internals.

  3. 3
    Split data ownership

    Separate schemas or table prefixes per module.

  4. 4
    Enforce rules in CI

    Fail builds on forbidden imports.

  5. 5
    Use events for side effects

    Decouple modules, easing future extraction.

07

Monolith vs modular monolith vs microservices

Three architectures

Step 1 / 5
AspectMonolithModular monolithMicroservices
DeploymentOne unitOne unitMany units
BoundariesWeakEnforced in codeEnforced by network
DataShared tablesOwned per moduleDatabase per service
CommunicationAny function callPublic APIs, in-process eventsHTTP, gRPC, messaging
Operational complexityLowLowHigh

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.

08

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..");
09

Complexity and performance

Module callIn-process

No network overhead.

Extraction effortLow-medium

Boundary already exists.

10

Trade-offs

Shared deployment

All modules still release together and share runtime resources; one memory leak affects all.

Discipline required

Boundaries are only as strong as the tooling and reviews enforcing them.

11

Variants and related techniques

Separate schemas per module

Stronger data isolation in one database.

Plugin architecture

Modules loaded dynamically.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Split a monolith into 4 modules with APIsMediumBoundaries.
Plan extraction of payments into a serviceHardStrangler pattern.