Skip to content

05 · Entities, values, services

When you finish this lesson you should be able to:

  • Decide entity vs value by asking what equality and continuity mean
  • Trim associations until they can be implemented (one way, small multiplicity)
  • Use a domain service without letting it become FooManager
  • Pack meaning into modules named from the language, not utils

Defining objects that capture the domain sounds intuitive. The shades of meaning are the whole chapter.

Evans’s fork:

  • Does this represent something with continuity and identity, tracked through state changes and even across implementations? → Entity
  • Or is it an attribute that describes the state of something else? → Value object
  • Or is it an action that you would distort if you glued it onto one object? → Service

Making that distinction explicit tells you how to implement: identity maps and repositories vs immutability and copy-by-value vs a stateless operation in the domain layer.

Associations: the trap that looks like analysis

Section titled “Associations: the trap that looks like analysis”

Associations are easy on a whiteboard and a quagmire in code. Every line you draw is:

  • a traversal you must be able to load
  • a consistency rule
  • a chance at a many-to-many explosion

Practices that keep MODEL-DRIVEN DESIGN viable:

  1. Impose a traversal direction. Bidirectional “for convenience” doubles the work. Prefer the direction of the invariant (Thread knows participants; Account does not hold all threads).
  2. Add qualifiers, reduce multiplicity. “User has messages” is unbounded. “Thread has messages” is still big; “thread has messages for this page of history” is a query, not a field.
  3. Avoid many-to-many in the model unless the relationship is a concept (then it is often an entity: Assignment is the relationship between Experiment and UserContext).
  4. Prefer ids at the boundary of aggregates (lesson 06) over live object webs.

If you cannot explain how this association is loaded and updated in one transaction, it is not ready to draw.

Identity is not “has a UUID column.” Identity is we care that it is the same thing over time.

  • Account: email changes, it is still this account.
  • InboxThread: new messages, same thread.
  • Experiment: variations change, same experiment (until you mean to fork it).

Implementation consequences:

  • Equality by identity, not by current fields.
  • You need a way to find them (repository).
  • You must be careful when merging systems (two databases, two ids for one person)—that is a bounded-context problem, not a hashCode problem.

Do not give identity to things that do not need it. Surrogate keys on every row train you to make everything an entity.

You care about what it is, not which copy. Money, QuietHours, PercentRollout, a date range, MessageBody, Variation key.

Practices:

  • Immutable. Replace the whole value.
  • Equality by structure.
  • Side-effect-free functions live here happily (lesson 09).
  • They can be tiny and numerous. That is fine.

If three call sites mutate hours.start, you never had a value. You had a bag.

When the database demands an id on every table, keep the id out of the model. Persistence is allowed to have columns the ubiquitous language does not speak.

Some domain meaning is a verb: AssignVariation, RouteToChannel, ConvertCurrency if conversion is a domain operation and not just an API call.

A domain service:

  • is named in the ubiquitous language
  • is stateless with respect to itself (it uses entities/values)
  • does not own the screen or the table

It is a slight departure from “everything is an object with data.” Forcing user.assignYourselfTo(experiment) can be cute and wrong.

Application services (lesson 04) are different: they load and save. Do not mix.

If your service needs seven repositories, you are missing an aggregate or a concept (lessons 06 and 08).

Not “a folder so the path is short.” A module is a named chunk of the model (identity, experiments, delivery) with a linguistic boundary. utils, common, helpers are where models go to die.

Modules also give you a place to look when the language splits—but they are not bounded contexts. Two modules in one context still share a language. See lesson 12.

Concept Kind Equality / notes
InboxThread Entity Same thread as replies accrue
Account Entity Identity context; referenced by id here
MessageId Value Opaque bytes
QuietHours Value {start, end, tz}
Variation Value control / treatment + payload
Assignment Entity if audited; value if pure function of salt This is a modeling choice—say it
DeliveryRouter Service Channel from preferences

Bad association: User.threads: Thread[].
Better: Thread.participantIds + ThreadRepository.forUser(id).

  1. Eight nouns from onboarding + experiments. Mark E / V / S. Write the equality rule when you hesitate.
  2. Draw associations. Delete every bidirectional convenience arrow.
  3. Turn one many-to-many into a named concept (probably an entity).
  4. Two modules. If a noun wants both, you have a homonym or a future context split.
  5. One domain service with two arguments. If you need a Context blob of twelve fields, the language is still thin.
  • Domain-Driven Design — Chapter 5, A Model Expressed in Software (associations; entities; value objects; services; modules)