The Architecture Reference

Ddd application · Domain-Driven Design · Advanced

Design Heuristics and Evolving Design

Answer DDD's perennial 'it depends' with a decision tree driven by subdomain type — then evolve those decisions as the business changes.

Ddd application Advanced ⏱ 4 min read Complete

🧭 Analogy

A heuristic is a seasoned chef’s “season to taste” — not a guaranteed-correct rule, but a rule of thumb that focuses on the cues that matter and ignores the noise. DDD’s design heuristics make explicit what the eternal “it depends” actually depends on.

The unified decision tree

A heuristic is a rule of thumb sufficient for your immediate goals — it focuses on the “swamping forces” and ignores noise. DDD chains several into one tree, all flowing from subdomain type and the complexity of the logic and data.

graph TD
Sub["Subdomain type + complexity"] --> BL["Business-logic pattern"]
BL --> ES["money/audit/analysis -> event-sourced"]
BL --> DM["complex logic -> domain model"]
BL --> AR["complex data -> active record"]
BL --> TS["else -> transaction script"]
ES --> A1["Architecture: CQRS"]
DM --> A2["Architecture: ports & adapters"]
AR --> A3["Architecture: layered + service layer"]
TS --> A4["Architecture: minimal layers"]
A2 --> T1["Tests: pyramid"]
A1 --> T1
A3 --> T2["Tests: diamond"]
A4 --> T3["Tests: reversed pyramid"]
  • Business-logic cascade — money/audit/deep-analysis → event-sourced; complex logic → domain model; complex data → active record; else → transaction script. Distinguish complex from simple by whether the ubiquitous language describes CRUD or intricate processes, rules, and invariants. A by-product: the chosen pattern validates your subdomain-type assumption (a “core” that fits active record is a signal to revisit).
  • Architecture follows logic — event-sourced → CQRS; domain model → ports & adapters; active record → layered with a service layer; transaction script → minimal three layers.
  • Testing follows both — domain models → testing pyramid (aggregates and value objects are perfect units); active record → testing diamond (logic spread across service + business layers); transaction script → reversed pyramid (end-to-end).

Reverse the mapping to validate it

A foolproofing trick: first pick the implementation pattern that fits the requirements (no gold-plating), then map it back to a subdomain type, then check against the business vision. A “core” you can hack in a day → look for finer subdomains. A “supporting” needing a domain model → either accidental complexity or a hidden competitive edge.

Sizing: start wide, decompose later

Size is “one of the least useful” heuristics for service boundaries — make the bounded context’s size a function of the model, not the model a function of the size. Because cross-context changes are expensive and refactoring physical boundaries is far costlier than logical ones, start with wider boundaries (especially for volatile, poorly-known core subdomains) and decompose as knowledge grows. The less you know about the domain, the wider the initial boundaries.

Design must evolve

Subdomains migrate between types in all six directions — core↔generic, core↔supporting, generic↔supporting — driven by whether the complexity is justified by advantage or profit, or whether a market solution has commoditized the capability (route optimization core→generic; Amazon→AWS generic→core).

graph LR
Core["Core"] <-->|"commoditized / re-invented"| Gen["Generic"]
Core <-->|"value rises / falls"| Sup["Supporting"]
Gen <-->|"customized / standardized"| Sup
Core -.->|"e.g. route opt → buyable"| Gen
Gen -.->|"e.g. Amazon → AWS"| Core

A type change ripples into strategic decisions (integration patterns, build vs buy) and tactical ones (the implementation pattern). Patterns migrate incrementally: transaction script → active record → domain model → event-sourced domain model. Migrating to event sourcing forces a choice between generating approximate past transitions (losing true history) and a single honest migrated-from-legacy event (leaving permanent legacy traces).

Don't ignore the pain

The clearest signal of a type change is that the current technical design can’t support business needs. The pain of adding functionality is diagnostic — if it’s the tactical design that hurts, the subdomain has likely evolved. Unmanaged growth turns sophisticated designs into a big ball of mud; treat change as a first-class design element and keep eliminating accidental complexity while managing essential complexity with DDD’s tools.

Heuristics, not dogma

Prefer the simplest tool that fits and escalate only when necessary — though context matters (some experienced teams use event sourcing everywhere because it’s simpler for them). Treat the decision trees as a starting framework, never a replacement for critical thinking.

See also

When to use it — and when not

✅ Reach for it when

  • When deciding the implementation pattern, architecture, and testing strategy for a subdomain.
  • When sizing a bounded context and you're unsure how wide to start.
  • When the 'pain' of extending a component signals that its design no longer fits.

⛔ Think twice when

  • Don't treat the heuristics as hard rules — they are rules of thumb that have exceptions.
  • Don't keep a design frozen — subdomains migrate between types and design must follow.

Check your understanding

Score: 0 / 4

1. According to the heuristics, what does 'it depends' mostly depend on?

The unified tactical decision tree starts from subdomain type and the complexity of logic and data, which then drive the business-logic pattern, architecture, and testing strategy.

2. What is the recommended starting size for a bounded context when the domain is poorly understood?

Cross-context changes are expensive and signal bad boundaries; volatile, poorly-known core subdomains should start with wider boundaries and be decomposed as knowledge grows.

3. Which testing strategy fits a domain model best?

Aggregates and value objects are perfect units, so the pyramid (many unit, fewer integration, fewest end-to-end) fits both domain-model variants; active record fits the diamond, transaction script the reversed pyramid.

4. What is the clearest signal that a subdomain has changed type?

The pain of adding functionality to a poorly-fitting design is diagnostic; a supporting subdomain growing complex is the classic supporting-to-core transition.

Comments

Sign in with GitHub to join the discussion.