Layering and Boundaries
Purpose
Give each boundary in the system a reason to exist, a direction, and a mechanism that
enforces it. Layers are the oldest structuring idea in enterprise software and the most
routinely cargo-culted: teams inherit three packages and a naming convention without the
constraint that made them worth having, and pay the indirection cost with none of the
benefit.
The two failures this exists to prevent: layers that are documentation only, so the domain
imports the web framework and nobody notices for a year; and layers multiplied past their
value, so a field addition touches an entity, a mapper, a DTO, a request, a response, a
service interface and its single implementation.
Workflow
- Find the boundaries that already exist, including the informal ones: a package
nobody outside touches, a class every feature edits, a schema owned by another team.
These are the real structure; the diagram is aspiration.
- For each candidate boundary, name what varies across it. A boundary earns its cost
when it isolates change, ownership, trust, invariants or a public contract. Co-change
is a reason to inspect its cost, not proof that these protections are unnecessary.
- Fix the dependency direction and write it down. Direction is the whole substance of
a layering decision; without it you have packages, not layers.
- Decide what crosses. The type that crosses a boundary is part of the boundary's
contract. Serializing a JPA entity directly can couple the HTTP representation to the
persistence model and lazy-loading behavior; inspect the actual mapping/serialization.
- Enforce mechanically. ArchUnit rules, module boundaries, or compilation units.
Check that forbidden dependencies actually fail; code review alone can miss violations.
- Recheck the count. Use representative change history and the boundary's protection
contract to assess value. Missing history, especially in a new system, is uncertainty;
remove a boundary only after checking callers, invariants and migration cost.
The classical split, stated as obligations
Presentation request/response shapes, protocol, formatting, validation of
input syntax, session and navigation. Knows the domain;
the domain does not know it.
│
▼
Domain business rules, invariants, workflow, calculations.
Should be readable without knowing whether the caller is
HTTP, a scheduled job or a message consumer.
│
▼
Data source persistence, external systems, transaction mechanics.
Knows the schema and the protocol; ideally does not know
the business rules that use it.
Two properties matter more than the diagram. Downward-only dependency: a lower layer
never names a higher one. Skip rules are a decision, not an accident: presentation
reaching data source directly is a legitimate, common choice for read paths, and a bad
accident when it happens to a write path (query-objects-and-specifications).
Decision rules
Two sides change for the same reason, at the same time, by the same team
→ investigate redundant separation; merge only if no independent
protection/contract justifies it and compatibility can be preserved.
Two sides differ in what they are about (business rules vs SQL dialect)
→ a boundary, enforced by dependency direction. Cheapest and
highest value: this is the classical layering split.
Two sides differ in ownership or release cadence
→ a boundary that must be enforced mechanically, because social
enforcement fails exactly when the two teams are busiest.
A dependency must point upward (domain needs to notify, to fetch, to
schedule)
→ invert it: the consuming domain/application layer owns the port, the outer layer
implements it. This makes the dependency follow the inside-owned
contract, as in ports and adapters.
An interface exists with exactly one implementation, no inversion, and no
second implementor in prospect
→ inspect whether it expresses an API, ownership or testing contract;
without such a purpose, consider removing the indirection
(enterprise-architecture-smells).
The boundary is between features rather than between technical concerns
→ consider vertical slices or modules; the layer packages will
otherwise scatter each feature across three places.
Rules
- A layer is defined by its dependency direction, not by its package name.
service,
repository and controller packages with imports flowing in both directions are a
naming convention with layering vocabulary attached.
- The domain layer's test is blunt and worth applying literally: could this code compile
and its tests run with the web framework and the ORM off the classpath? Where the answer
is no, name the specific import and decide whether it is a leak or an accepted trade.
- Do not confuse layers (a dependency rule) with tiers (a deployment topology).
Layering is a source-code decision with indirection and migration costs. Tiers add a
network, serialisation and partial failure, making changes operationally more involved
(
distribution-boundaries). Neither is cost-free or inherently irreversible.
- The type that crosses a boundary is the contract. Decide deliberately whether it is the
domain type, a dedicated representation, or a projection; each choice is defensible and
the failure is choosing by default (
remote-facade-and-dto).
- Layer count is a cost. Three layers with real constraints beat six with none. Every
additional layer multiplies mapping code, obscures stack traces and lengthens the change
path; it must buy something nameable.
- The read path and the write path are allowed to differ. Writes benefit from going
through the domain to protect invariants; reads frequently do not, and forcing every
query through an aggregate is a leading cause of N+1 and of over-fetching
(
architecture-and-performance).
- Hexagonal, clean and onion architectures share an inward-dependency principle —
outward dependencies are inverted through interfaces the inside owns — with different
vocabularies and different prescriptions for application/domain structure. Name the
concrete dependency rules rather than assuming the labels are interchangeable.
- Adopting one of those styles is a decision with drivers, not a default. The driver is
usually "the domain must be testable and outlive this framework" or "we will replace
this integration". Without such a driver you are buying mapping code.
- A boundary you cannot violate accidentally is worth more than a boundary described in a
wiki. Prefer compiler and build-time enforcement; a failing ArchUnit test is the
cheapest architecture governance available.
References
- Layering styles compared — classical three-layer,
hexagonal/ports-and-adapters, clean, modular monolith and vertical slices side by side:
what each actually constrains, what it costs, the driver that justifies it, and where
classical layering is still the right answer. Read when the style itself is the question,
or when a team proposes adopting one.
- Enforcing a boundary — package layout that makes
violations visible, ArchUnit and JPMS enforcement with concrete rules, what may cross a
boundary and in which direction, and the seven recurring leaks (entity in the web layer,
framework annotations in the domain, transaction demarcation in the wrong place, and the
rest). Read when designing the package structure or auditing an existing one.
1---2name: layering-and-boundaries3description: Deciding where an enterprise application's boundaries go and which direction dependencies cross them: the classical presentation / domain / data-source split, the styles that reorganise it (hexagonal, clean, modular monolith, vertical slices), and how a boundary is enforced rather than documented. Use when a package structure is argued about, when a controller contains business rules, when an entity or DTO travels end to end, when a service layer only forwards, when hexagonal is adopted without a driver, or when adding a field requires editing seven files. Does not cover which layer business rules take (domain-logic-organization), whether a boundary should be remote (distribution-boundaries), the data-access patterns (data-source-patterns), or what the application service around a use case owns (service-layer-design).4---56# Layering and Boundaries78## Purpose910Give each boundary in the system a reason to exist, a direction, and a mechanism that11enforces it. Layers are the oldest structuring idea in enterprise software and the most12routinely cargo-culted: teams inherit three packages and a naming convention without the13constraint that made them worth having, and pay the indirection cost with none of the14benefit.1516The two failures this exists to prevent: layers that are documentation only, so the domain17imports the web framework and nobody notices for a year; and layers multiplied past their18value, so a field addition touches an entity, a mapper, a DTO, a request, a response, a19service interface and its single implementation.2021## Workflow22231. **Find the boundaries that already exist**, including the informal ones: a package24 nobody outside touches, a class every feature edits, a schema owned by another team.25 These are the real structure; the diagram is aspiration.262. **For each candidate boundary, name what varies across it.** A boundary earns its cost27 when it isolates change, ownership, trust, invariants or a public contract. Co-change28 is a reason to inspect its cost, not proof that these protections are unnecessary.293. **Fix the dependency direction and write it down.** Direction is the whole substance of30 a layering decision; without it you have packages, not layers.314. **Decide what crosses.** The type that crosses a boundary is part of the boundary's32 contract. Serializing a JPA entity directly can couple the HTTP representation to the33 persistence model and lazy-loading behavior; inspect the actual mapping/serialization.345. **Enforce mechanically.** ArchUnit rules, module boundaries, or compilation units.35 Check that forbidden dependencies actually fail; code review alone can miss violations.366. **Recheck the count.** Use representative change history and the boundary's protection37 contract to assess value. Missing history, especially in a new system, is uncertainty;38 remove a boundary only after checking callers, invariants and migration cost.3940## The classical split, stated as obligations4142```text43Presentation request/response shapes, protocol, formatting, validation of44 input syntax, session and navigation. Knows the domain;45 the domain does not know it.46 │47 ▼48Domain business rules, invariants, workflow, calculations.49 Should be readable without knowing whether the caller is50 HTTP, a scheduled job or a message consumer.51 │52 ▼53Data source persistence, external systems, transaction mechanics.54 Knows the schema and the protocol; ideally does not know55 the business rules that use it.56```5758Two properties matter more than the diagram. **Downward-only dependency:** a lower layer59never names a higher one. **Skip rules are a decision, not an accident:** presentation60reaching data source directly is a legitimate, common choice for read paths, and a bad61accident when it happens to a write path (`query-objects-and-specifications`).6263## Decision rules6465```text66Two sides change for the same reason, at the same time, by the same team67 → investigate redundant separation; merge only if no independent68 protection/contract justifies it and compatibility can be preserved.6970Two sides differ in what they are about (business rules vs SQL dialect)71 → a boundary, enforced by dependency direction. Cheapest and72 highest value: this is the classical layering split.7374Two sides differ in ownership or release cadence75 → a boundary that must be enforced mechanically, because social76 enforcement fails exactly when the two teams are busiest.7778A dependency must point upward (domain needs to notify, to fetch, to79schedule)80 → invert it: the consuming domain/application layer owns the port, the outer layer81 implements it. This makes the dependency follow the inside-owned82 contract, as in ports and adapters.8384An interface exists with exactly one implementation, no inversion, and no85second implementor in prospect86 → inspect whether it expresses an API, ownership or testing contract;87 without such a purpose, consider removing the indirection88 (enterprise-architecture-smells).8990The boundary is between features rather than between technical concerns91 → consider vertical slices or modules; the layer packages will92 otherwise scatter each feature across three places.93```9495## Rules9697- A layer is defined by its dependency direction, not by its package name. `service`,98 `repository` and `controller` packages with imports flowing in both directions are a99 naming convention with layering vocabulary attached.100- The domain layer's test is blunt and worth applying literally: could this code compile101 and its tests run with the web framework and the ORM off the classpath? Where the answer102 is no, name the specific import and decide whether it is a leak or an accepted trade.103- Do not confuse **layers** (a dependency rule) with **tiers** (a deployment topology).104 Layering is a source-code decision with indirection and migration costs. Tiers add a105 network, serialisation and partial failure, making changes operationally more involved106 (`distribution-boundaries`). Neither is cost-free or inherently irreversible.107- The type that crosses a boundary is the contract. Decide deliberately whether it is the108 domain type, a dedicated representation, or a projection; each choice is defensible and109 the failure is choosing by default (`remote-facade-and-dto`).110- Layer count is a cost. Three layers with real constraints beat six with none. Every111 additional layer multiplies mapping code, obscures stack traces and lengthens the change112 path; it must buy something nameable.113- The read path and the write path are allowed to differ. Writes benefit from going114 through the domain to protect invariants; reads frequently do not, and forcing every115 query through an aggregate is a leading cause of N+1 and of over-fetching116 (`architecture-and-performance`).117- Hexagonal, clean and onion architectures share an inward-dependency principle —118 outward dependencies are inverted through interfaces the inside owns — with different119 vocabularies and different prescriptions for application/domain structure. Name the120 concrete dependency rules rather than assuming the labels are interchangeable.121- Adopting one of those styles is a decision with drivers, not a default. The driver is122 usually "the domain must be testable and outlive this framework" or "we will replace123 this integration". Without such a driver you are buying mapping code.124- A boundary you cannot violate accidentally is worth more than a boundary described in a125 wiki. Prefer compiler and build-time enforcement; a failing ArchUnit test is the126 cheapest architecture governance available.127128## References129130- [Layering styles compared](references/layering-styles.md) — classical three-layer,131 hexagonal/ports-and-adapters, clean, modular monolith and vertical slices side by side:132 what each actually constrains, what it costs, the driver that justifies it, and where133 classical layering is still the right answer. Read when the style itself is the question,134 or when a team proposes adopting one.135- [Enforcing a boundary](references/boundary-enforcement.md) — package layout that makes136 violations visible, ArchUnit and JPMS enforcement with concrete rules, what may cross a137 boundary and in which direction, and the seven recurring leaks (entity in the web layer,138 framework annotations in the domain, transaction demarcation in the wrong place, and the139 rest). Read when designing the package structure or auditing an existing one.