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

By Cobnex editorial. Published 2026-09-10. Updated 2026-09-11.

## 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 contract

The caller uses a business operation while the order module keeps its rules and persistence internal.

1. **Support feature**: Requests an address change for a known order
2. **Order contract**: Accepts a narrow command and caller context
3. **Order rules**: Checks authority, dispatch state and version
4. **Owned persistence**: Commits 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.

## Sources

- [Microsoft Learn: common web application architectures](https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures)
- [Martin Fowler: Monolith First](https://martinfowler.com/bliki/MonolithFirst.html)
