Distinguishing Entities vs Value Objects in DDD: Criteria & Code
Learn how to tell whether a domain concept is an Entity or a Value Object by asking if its identity matters or only its attributes, see concrete examples with Money, Address and Order, understand immutable design, proper equals/hashCode implementation, and validation through unit tests.
Entity vs. Value Object decision rule
Ask whether the domain concept is identified by a unique identity that must be tracked ( Entity ) or whether only its attribute values matter ( Value Object ).
If you need to know which one it is – unique ID, lifecycle tracking – treat it as an Entity .
If you only need to know what it looks like – all fields matter, no identity – treat it as a Value Object .
Illustrative examples
Money – two 100‑yuan notes are interchangeable; the amount is a value object because only the numeric value and currency matter.
Order – even if two orders have identical amounts and items, they are distinct because each has a unique order number that drives its state changes (pending → paid → shipped). The order number is the identity, so Order is an Entity.
Core concepts
Identity : whether “the same thing” matters (e.g., Order ID, Member ID).
Attributes : the observable data (e.g., amount, currency, province, city).
Lifecycle : whether the object goes through state transitions (e.g., Order moves from pending to paid).
Entity characteristics
Has a unique identifier (ID) used to determine sameness.
Has a lifecycle; its attributes may change over time.
Equality is based solely on the ID, even if other fields differ.
Java example (Entity equality based on ID):
public class Order {
private final OrderId id; // immutable identity
private OrderStatus status; // mutable lifecycle state
private List<OrderItem> items;
private Money totalAmount;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
return id.equals(((Order) o).id);
}
@Override
public int hashCode() { return id.hashCode(); }
}Value Object characteristics
No independent ID; equality is based on all attribute values.
Immutable – once created it cannot change; any modification yields a new instance.
Can be freely copied, shared, and replaced.
Immutable Money value object (centralized validation, arithmetic returns new instances):
public final class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
if (amount == null || currency == null) {
throw new IllegalArgumentException("Amount and currency cannot be null");
}
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = currency;
}
public static Money of(BigDecimal amount, Currency currency) { return new Money(amount, currency); }
public static Money ofYuan(BigDecimal yuan) { return new Money(yuan, Currency.getInstance("CNY")); }
public Money add(Money other) { requireSameCurrency(other); return new Money(this.amount.add(other.amount), this.currency); }
public Money multiply(int quantity) { return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency); }
public Money subtract(Money other) { requireSameCurrency(other); return new Money(this.amount.subtract(other.amount), this.currency); }
private void requireSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money m = (Money) o;
return amount.equals(m.amount) && currency.equals(m.currency);
}
@Override public int hashCode() { return Objects.hash(amount, currency); }
}Immutable Address value object (validation in constructor, no setters):
public final class Address {
private final String province;
private final String city;
private final String district;
private final String detail;
private final String receiver;
private final String phone;
public Address(String province, String city, String district, String detail, String receiver, String phone) {
if (phone == null || !phone.matches("\\d{11}")) {
throw new IllegalArgumentException("Invalid phone format");
}
this.province = province;
this.city = city;
this.district = district;
this.detail = detail;
this.receiver = receiver;
this.phone = phone;
}
public String fullAddress() { return province + city + district + detail; }
// equals/hashCode compare all fields (omitted for brevity)
}Benefits of using Value Objects
Business rules such as “amount cannot be negative”, “currency must match”, and “scale = 2 with HALF_UP rounding” are enforced in the constructor, guaranteeing consistency across the code base.
Code becomes safer and more expressive: total = total.add(item.subtotal()) replaces scattered BigDecimal arithmetic.
Immutability allows free sharing without risk of accidental mutation.
Context‑dependent classification
The same concept may be an Entity in one bounded context and a Value Object in another. For example, an address stored in a member profile (with an ID and CRUD operations) is an Entity, while the address snapshot captured on an order is a Value Object.
Validation through unit tests
Write minimal tests that exercise identity‑based equality for Entities and attribute‑based equality for Value Objects.
class MoneyTest {
@Test void sameAmountAndCurrencyMeansSameValue() {
assertEquals(Money.ofYuan(new BigDecimal("10.00")),
Money.ofYuan(new BigDecimal("10.00")));
}
@Test void cannotAddDifferentCurrencies() {
Money cny = Money.of(new BigDecimal("10.00"), Currency.getInstance("CNY"));
Money usd = Money.of(new BigDecimal("10.00"), Currency.getInstance("USD"));
assertThrows(IllegalArgumentException.class, () -> cny.add(usd));
}
}
class OrderTest {
@Test void entityEqualityUsesIdentityOnly() {
OrderId id = new OrderId(1001L);
Order a = Order.restore(id, OrderStatus.PENDING_PAYMENT);
Order b = Order.restore(id, OrderStatus.PAID);
assertEquals(a, b); // same ID → same entity
}
}Common pitfalls
Turning everything into an Entity – only create an Entity when identity tracking is truly required.
Adding setters to Value Objects – they must remain immutable; changes require a new instance.
Implementing Entity equality by comparing all fields – equality should rely solely on the ID.
Worrying about persisting Value Objects – they are stored as embedded columns (e.g., JPA @Embeddable or mapped to multiple columns in MyBatis).
Aggregation reminder
Entities and Value Objects collaborate to enforce business invariants such as “order total must equal the sum of line items”. Managing that consistency is the core challenge of DDD and is addressed through Aggregates.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Yumin Fish Harvest
A deep‑sea salvage fisherman sharing architecture insights, practical tips, and lessons learned.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
