Why: the goal or problem

Features interfere, but separate services add unwanted operating work. You need boundaries within one deployment.

How: work toward a solution

  1. Fictional app v1: one team deploys Catalog, Library and Billing together; A17 has no tag.
  2. Library adds tag storage in its owned tables. Deploy app v2; Billing behavior stays unchanged. One database holds module-owned tables; direct cross-module access is forbidden.
  3. Library calls Catalog’s API in-process, then writes travel. A lookup error leaves no tag; retry after recovery.
Internal contracts, shared release

Illustrative example

Internal contracts, shared release

Reading app · one team · A17 has no tag

Change: add a Library tag; Billing behavior stays unchanged.

Deployment unit · 1

App v1 → App v2
  1. Catalog

    APIArticle lookup
    Owned internalsCatalog-owned tables
  2. Library

    Tag feature added
    APISet A17 tag: travel
    Owned internalsLibrary-owned tables + tag field
  3. Billing

    APIBehavior unchanged
    Owned internalsBilling-owned tables

Library → Catalog API · In-process call

No direct access to another module's tables. Calls enter through its API.

One shared database; separate table ownership. Library changes its storage inside the common release.

  1. Lookup succeeds → write travel → A17 has tag travel.
  2. Lookup fails before writing → no tag. Report failure; retry after recovery.

A monolith subtype: one release and shared process failures. Enforce API boundaries with code rules and tests; folders alone cannot protect them.

Ownership boxes describe access rules, not database-machine counts.

What: the concept

A modular monolith combines one deployment with deliberate internal boundaries. Module APIs protect owned internals. It is still a monolith. Fowler

Enforce boundaries beyond folders; releases and process failures remain shared. Modules can use hexagonal ports.