Entities & Value Objects
Die zwei Arten von Domänenobjekten: Entities mit Identität, die sich über die Zeit ändert, und unveränderliche Value Objects, die nur durch ihre Attribute definiert sind (Money, DateRange).
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
DDD divides domain objects into two fundamental kinds, and getting the distinction right is the difference
between a clean model and a tangle of primitives. An entity has an identity that persists through change; a
value object is defined purely by its attributes and is immutable. Most bugs in modeling come from treating a
value as an entity, or - far more commonly - not modeling values at all and drowning in raw Strings and
BigDecimals.
Entities: identity that outlives change
An entity is something the business tracks as the same thing over time, even as its attributes change. A
Transaction in ledger-legacy is an entity: its amount might be corrected, its status moves from pending to
settled, but it's still the same transaction, identified by its id.
class Transaction { // an ENTITY - has identity
private final TransactionId id; // identity, set once, never changes
private Money amount; // attributes CAN change over time
private TransactionStatus status;
// equality is by IDENTITY, not attributes
public boolean equals(Object o) {
return o instanceof Transaction t && this.id.equals(t.id);
}
public int hashCode() { return id.hashCode(); }
}The defining traits: a stable identity (an id assigned at creation), equality by that identity (two transactions are "the same" iff same id, regardless of current attributes), and a lifecycle (created, modified, archived). Ask: if all its attributes changed, would the business still call it the same thing? If yes, it's an entity.
Value objects: defined by what they are
A value object has no identity - it's entirely described by its attributes, and two with equal attributes
are interchangeable. Money, a DateRange, an Address - $50 USD is $50 USD; there's no "which fifty
dollars." Value objects are immutable and compared by value:
record Money(BigDecimal amount, Currency currency) { // a VALUE OBJECT
Money { // validate invariants at creation
if (amount.scale() > currency.getDefaultFractionDigits())
throw new IllegalArgumentException("too many decimal places");
}
Money add(Money other) { // operations return NEW instances
requireSameCurrency(other);
return new Money(amount.add(other.amount), currency); // no mutation
}
}
// equality is automatic and by-value for records: Money(50,USD).equals(Money(50,USD)) == trueJava records are the perfect fit: immutable, value-based equals/hashCode for free. Operations don't mutate;
they return new instances (like String or LocalDate).
Why value objects matter so much
The most impactful DDD-lite habit is replacing primitives with value objects - the cure for "primitive obsession." Compare:
void transfer(BigDecimal amount, String currency, String fromAccount, String toAccount) // 🚩 all primitives
void transfer(Money amount, AccountId from, AccountId to) // ✅ typed valuesThe second version:
- Can't be called wrong - you can't accidentally swap
fromandtoif they're both rawStrings... but withAccountIdtypes plus clear names, and impossible to pass a currency where an account goes. - Centralizes validation -
Money's constructor rejects negative or over-precise amounts once, so no caller can construct an invalid one. - Carries behavior -
money.add(other)enforces currency-matching; scatteredBigDecimalarithmetic doesn't. - Makes illegal states unrepresentable - a
DateRangethat guarantees start ≤ end can't express an inverted range.
Prefer value objects; they're cheap and safe
When in doubt, model a concept as an immutable value object rather than a bag of primitives. They're side-effect free, trivially testable, thread-safe, and free of identity bookkeeping. A good heuristic: if you find yourself passing the same two or three primitives around together (amount+currency, lat+lng, start+end), that cluster wants to be a value object.
A banknote is a value object: a $20 bill is interchangeable with any other $20 bill - you don't care which twenty you're handed, only that it's twenty dollars, and you'd never track its lifecycle. A bank account is an entity: it has an account number (identity) that stays the same as the balance rises and falls, transactions post, and the address on file changes - it's the same account throughout, and 'same account' means same number, not same balance. Confusing the two - tracking individual banknotes by serial number, or treating two accounts with equal balances as identical - is where modeling goes wrong.
Classify each concept in a ledger system as an entity or a value object, and justify: (1) a Customer, (2) a postal Address, (3) an Invoice, (4) a Money amount, (5) an ISO Currency code. For the value objects, note one benefit of modeling it as a type instead of a primitive.
What's the defining difference between an entity and a value object?
Key takeaways
- Entities have a stable identity that persists through change; equality is by id, and they have a lifecycle (Transaction, Customer, Invoice).
- Value objects have no identity - they're immutable and defined entirely by their attributes; equality is by value (Money, Address, DateRange).
- Java records are ideal for value objects: immutable with value-based equals/hashCode for free; operations return new instances.
- Replacing primitive clusters with value objects cures 'primitive obsession' - centralizing validation, carrying behavior, and making illegal states unrepresentable.
- Test: if all attributes changed, would the business call it the same thing? Yes → entity; interchangeable by attributes → value object.