# Modules With Real Boundaries — Monolith vs Microservices

Source: https://www.skillbyai.com/en/monolith-microservices/m-modules

> 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.](assets/figures/monolith-microservices/section-3-map.svg) — 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.

```java
@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.

**Quiz:** What makes a modular monolith different from a big ball of mud?

- [x] 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.
