Lesson 7 / 25

Modules With Real Boundaries

Structure a monolith into modules with public APIs and enforced dependencies.

Microservice-style boundaries, monolith-style deployment

A modular monolith divides the code into modules aligned with business capabilities (catalogue, ordering, billing), each with a small public API and hidden internals. Other modules may call only that API, never internal classes. Dependencies form a directed acyclic graph: ordering may depend on catalogue, but not the reverse. Enforce the rules automatically, because conventions erode: Java teams use ArchUnit tests, the Java module system, or Spring Modulith; .NET teams use separate projects with internal visibility and architecture tests; TypeScript monorepos use tools such as Nx module boundaries or dependency-cruiser. Modules communicate through direct calls on their API, or through in-process events for loose coupling. The result keeps the simplicity of one deployment while preparing clean seams if a module ever needs to become a service.

Modules with public APIs

Each module exposes a small API; internal parts are hidden from other modules.

A large box divided into four compartments, each with a small doorway on its edge; arrows between compartments pass only through the doorways.
Figure 3.1 — A modular monolith with enforced module APIs.

Enforcing module boundaries with ArchUnit (Java)

The build fails if billing code reaches into ordering internals.

@AnalyzeClasses(packages = "com.shop")
class ModuleBoundaryTest {

    @ArchTest
    static final ArchRule only_api_packages_are_used_across_modules =
        classes().that().resideInAPackage("com.shop.ordering.internal..")
            .should().onlyBeAccessed().byAnyPackage("com.shop.ordering..");

    @ArchTest
    static final ArchRule catalogue_does_not_depend_on_ordering =
        noClasses().that().resideInAPackage("com.shop.catalogue..")
            .should().dependOnClassesThat().resideInAPackage("com.shop.ordering..");
}

Make illegal dependencies fail the build

A boundary that is only documented will be crossed under deadline pressure. A failing test in CI is much harder to ignore.

Quick check: What makes a modular monolith different from a big ball of mud?

  • Modules have explicit public APIs and enforced dependency rules
  • It is deployed as several services
  • It uses a NoSQL database
  • It has no database
Answer

Modules have explicit public APIs and enforced dependency rules — Enforced module boundaries are the defining property; deployment is still a single unit.