100%

Lesson 2 of 2

What It Costs, and Three Ways Teams Get Nothing For It

The rings are expensive. This lesson prices them honestly, then examines the anaemic domain, the mapping explosion, and rings applied uniformly to code that has no rules.

10 min read

Clean architecture is currently the most copied structure in the industry, and a large share of the copies get the file layout without the benefit. That is not because the pattern is wrong; it is because the benefit comes from a property — an independently testable, framework-free core — that is easy to lose while the diagram still looks right.

The same rule, once as an anaemic bag and once as an entity that can refuse.typescript
// Anaemic: the rule lives in whichever service remembered to check.
class Order { discount = 0; subtotal = 0; }

function applyDiscount(order: Order, amount: number) {
  if (amount > order.subtotal) throw new Error('too large'); // and in three other services
  order.discount = amount;
}

// Rich: the object cannot be put into an invalid state by anybody.
class Order {
  private constructor(readonly subtotal: Money, readonly discount: Money) {}

  withDiscount(amount: Money): Order {
    if (isGreaterThan(amount, this.subtotal)) {
      throw new DomainError('Discount cannot exceed the order subtotal');
    }
    return new Order(this.subtotal, amount);
  }
}
BuysCosts
Business rules testable in milliseconds with no infrastructureMore files and more indirection for every feature
Framework and database replaceable without touching rulesBoundary-crossing shapes and the mappers to maintain them
Rules survive a delivery-mechanism rewrite (web to API to job)A learning curve that slows new joiners for weeks
Modules become extractable, because dependencies are explicitEasy to apply uniformly and pay the cost where there is no benefit
Reviewable, enforceable structure for a growing teamAttractive enough that teams adopt the diagram without the discipline
An honest ledger. Whether the pattern is worth it is a question about your specific system, and these are the entries.

In practice

Adopting the rings where they pay, and not elsewhere

A team of twelve maintained a scheduling product with three genuinely complex areas — shift rules, pay calculation and compliance limits — and roughly forty screens of straightforward administrative reads and writes. A new lead proposed clean architecture across the codebase.

Constraints

  • Shift and pay rules change monthly and are legally significant
  • Administrative screens are simple and rarely change
  • Team includes four engineers in their first two years of work
  • A rewrite is not on the table; adoption must be incremental

Decision

Apply the full ring structure to the three rule-bearing modules only. Leave the administrative screens as a thin handler calling a repository directly. Write down the rule for which is which, and enforce the ring boundaries in the three modules with a build check.

Why

The value of the rings comes from protecting rules that are complex, valuable and frequently changed. The three modules have all of those properties and the administrative screens have none of them. Applying the structure uniformly would have cost roughly four extra files per screen across forty screens, and would have made the codebase harder for the four junior engineers without protecting anything.

What it cost

Two structures in one codebase requires a written rule and a conversation at the start of each piece of work, and the boundary between "has rules" and "does not" is genuinely fuzzy in a handful of cases. Twice in the first year a screen that started administrative grew rules and had to be migrated, costing about a week each time. The team judged two migrations cheaper than 160 files of ceremony, and recorded the calculation so the next lead can revisit it with the same numbers.

Being handed a clean-architecture template repository

As a developer

Adopts the structure and follows it faithfully, because the layout is clear, the community endorses it, and consistency with a known pattern is easier to defend in review than a bespoke arrangement.

As an architect

Asks what property the structure is meant to deliver here, and how the team will know if it is being delivered. Concretely: can the domain be tested with no infrastructure today, and will a build check keep that true next year? If the answer is no, the repository is a folder layout, and the honest options are to fix the enforcement or to stop paying for a structure that is not doing its job.

A team says they use clean architecture. Their entities carry ORM annotations, and their use cases return those entities directly to controllers, which serialise them to JSON. What have they got, and what would you fix first?Reveal

They have a folder layout. The annotations make the innermost ring depend on an outer-ring library, so the core cannot be compiled or tested without the persistence tool, and returning entities to the controller means the serialised API response is now the shape of a database table — a schema change becomes a breaking API change for every consumer. Fix the second problem first, because it is the one with external consequences and the one that is cheapest to fix incrementally: introduce output shapes owned by the use case, one endpoint at a time. Removing the annotations is more valuable in the long run and is a larger change, since it usually means introducing a separate persistence model and a mapper — which is the moment to check that the mapping is protecting something rather than being added because the pattern says so.

Key takeaways

  • The benefit is an independently testable, framework-free core; if you do not have that, you have a folder layout.
  • An entity with no method that can fail is a sign the rules live elsewhere and will be duplicated.
  • Collapse boundary shapes that always change together; keep the ones that are published contracts.
  • The rings cost the same everywhere and pay only where rules live, so applying them uniformly is a choice to be justified.
  • Enforce the dependency rule in the build, and treat an unenforced version as documentation only.