Skip to content

07 · Walk a model

When you finish this lesson you should be able to:

  • Run Evans’s chapter 7 sequence without using his cargo domain
  • Narrate a user action as a chain of root methods
  • Change one unplanned feature by extending the language, and notice what breaks
  • Treat the first aggregate cut as a guess you expect to refactor

Evans spends a long chapter on cargo shipping: apps vs domain, entities vs values, associations, aggregate boundaries, repositories, scenarios, factories, a pause to redesign the cargo aggregate, modules, then a new feature (allocation checking).

You do not need cargo. You need the order of operations:

  1. Isolate the domain (applications are thin).
  2. Distinguish entities and values.
  3. Design associations you can implement.
  4. Draw aggregate boundaries.
  5. Select repositories (roots only).
  6. Walk scenarios as method calls.
  7. Make creation go through factories.
  8. Pause. If the aggregate is awkward, redesign it before adding more.
  9. Place modules according to language.
  10. Add a feature you did not start with. The language should stretch, not snap.

A walk that never adds an unplanned feature is a canned demo. The allocation-checking moment in the book is the point: a new word in the expert’s mouth should already have a home, or you learn the model is shallow.

Applications: PostToThread, MarkRead, RequestPush. No business ifs.

Entities: InboxThread, Account (id from identity context).

Values: MessageBody, QuietHours, Channel.

Associations: Thread → participant ids (one way). Not User → all threads.

Aggregates: InboxThread (messages, cursors). PushDelivery separate.

Repositories: ThreadRepository, DeliveryRepository.

Scenario — reply

  1. threads.get(id)
  2. thread.append(authorId, body) — asserts participant, not archived
  3. threads.add(thread)
  4. If notification policy allows, application records DeliveryRequested

Pause for redesign. If append is also computing mute windows, quiet hours, and experiment holdout, the thread aggregate swallowed two other domains. Pull NotificationPolicy (value or small aggregate per user) back out. That pause is the chapter.

Unplanned feature — snooze 24h.
Wrong: snoozedUntil on every message.
Better: MuteWindow on the participant inside the thread, or policy aggregate per user. Walk both. Keep the invariant you can say in one sentence.

sequenceDiagram
  participant App
  participant Repo
  participant Thread
  App->>Repo: get threadId
  Repo-->>App: InboxThread
  App->>Thread: append author body
  App->>Repo: add thread
  App->>App: request push if policy allows

If append knows SMTP, isolation failed. Go back to lesson 04.

  • Loading one thread does not load the company.
  • You cannot getMessage from a global store without the thread id (local identity).
  • A new engineer can walk the scenario from the type names.

If any of those fail, the walk found a bug in the model, which is the successful outcome.

Holdout on first session.

  1. Ten minutes: entities, values, two aggregates, two repositories, one factory, one module map.
  2. Five minutes: walk “user opens first session and must stick to variation.”
  3. Five minutes: add “kill switch off for everyone within minutes.”
  4. Five minutes: write what broke. Park that sentence for lessons 08–12. Do not patch it with a boolean named kill_switch_hack.
  • Domain-Driven Design — Chapter 7, Using the Language: An Extended Example (the full walk; pause and redesign; new feature as test of the language)