Modular monolith boundaries

A modular monolith needs boundaries the code can enforce

One deployable application can contain clear business modules. The separation becomes useful when callers cannot casually bypass each module's rules and data ownership.

In this article

A folder is a starting point

An order module owns the rule that a dispatched order cannot change its delivery address. A support feature imports the order repository directly and updates the address column. The files live in separate folders, but the business boundary has already failed.

A module should expose the operations other parts of the application may use, while keeping implementation details private. In this example, callers request an address change through an order operation that checks eligibility. They do not receive unrestricted access to the order's persistence layer.

The deployment remains a monolith because the application ships as one unit. Modularity describes how responsibilities and dependencies are organised inside that unit. These are separate decisions, and neither automatically implies the other.

Organise around a capability

Choose boundaries that group business rules which change together. Orders, billing and scheduling may be useful candidates in one system, but the names are not universal. Discover the boundary by following real workflows and asking which rules need a consistent owner.

Keep the public contract small. An operation such as requestAddressChange communicates intent and can return a meaningful rejection. A generic updateEntity function exposes storage shape and makes it harder to preserve the rule as the domain evolves.

Enforce imports through the language or build tools available to the project. Package exports, visibility rules and dependency checks can prevent accidental access to internals. They do not replace review, but they make the boundary more than a convention people must remember.

An address change crosses a public contractThe caller uses a business operation while the order module keeps its rules and persistence internal.
  1. Support featureRequests an address change for a known order
  2. Order contractAccepts a narrow command and caller context
  3. Order rulesChecks authority, dispatch state and version
  4. Owned persistenceCommits the permitted change and records the result

Decide how modules share data

A single database can support a modular application, but unrestricted cross-module writes undermine ownership. Define which module changes each record and how other modules obtain the information they need.

Read models may deliberately combine data for a screen or report. Keep those queries distinguishable from mutation paths. A reporting join does not automatically justify updating another module's tables.

Transactions across modules can be practical within one process and database. Document the business invariant they protect. If a future service split is considered, that invariant will need an explicit distributed design rather than simply replacing a method call with HTTP.

Split deployment only for a concrete reason

Independent scaling, release cadence or ownership may eventually justify extracting a service. Before doing so, inspect the public contract, data ownership and transaction dependencies. A module with many direct table writes from elsewhere is not ready merely because its folder is neatly named.

A modular monolith can remain the right long-term architecture. It avoids some network and operational complexity while allowing disciplined internal design. The useful goal is a system whose changes and rules have clear homes, not a predetermined number of deployable services.

Primary sources

Microsoft Learn: common web application architecturesMartin Fowler: Monolith First

References checked 11 September 2026.