Fundamentals 14 min read

Entity vs Value Object: Why Most Developers Misinterpret Their Core Difference

The article explains that an Entity is distinguished by a unique identifier while a Value Object is defined solely by its attributes and must be immutable, illustrating the concepts with Java Entity, record, and JPA mapping examples, and providing three practical questions to decide which to use.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Entity vs Value Object: Why Most Developers Misinterpret Their Core Difference

Entity vs Value Object – Core Distinction

Two thought‑experiments are used to introduce the difference: a 100‑yuan banknote in two wallets (same value, different identity) and two employees named "Zhang Wei" with different employee numbers (different identities). The first shows that value objects are interchangeable, the second that entities must not be confused.

Entity: Identity‑Based Equality

An Entity has a unique identifier; two instances are considered different even if all other fields match. In a payroll system, RegularEmployee is an entity because each employee is tracked by an EmployeeId. The equality implementation compares only the id field:

public class RegularEmployee {
    private final EmployeeId id; // identity – the soul of the entity
    private PersonName name;
    private Money baseSalary;
    private PerformanceLevel performanceLevel;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof RegularEmployee)) return false;
        RegularEmployee that = (RegularEmployee) o;
        return id.equals(that.id); // compare only id
    }

    @Override
    public int hashCode() {
        return id.hashCode(); // hash only id
    }
}

If two employees have identical attributes but different IDs, equals returns false. The article warns that IDE‑generated equals/hashCode that include all fields break this rule and cause unexpected deduplication in collections.

Value Object: Attribute‑Based Equality

A Value Object has no identity; two instances are equal when all their attributes are equal. The Money class demonstrates this:

public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    private Money(BigDecimal amount, Currency currency) {
        if (amount == null) throw new IllegalArgumentException("金额不能为 null");
        if (amount.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("金额不能为负数");
        this.amount = amount.setScale(2, RoundingMode.HALF_UP);
        this.currency = currency;
    }

    public static Money ofYuan(BigDecimal amount) { return new Money(amount, Currency.CNY); }
    public static Money ofYuan(String amount) { return new Money(new BigDecimal(amount), Currency.CNY); }

    public Money add(Money other) { assertSameCurrency(other); return new Money(this.amount.add(other.amount), this.currency); }
    public Money subtract(Money other) { assertSameCurrency(other); return new Money(this.amount.subtract(other.amount), this.currency); }
    public Money multiply(BigDecimal factor) { return new Money(this.amount.multiply(factor), this.currency); }
    public boolean isGreaterThan(Money other) { assertSameCurrency(other); return this.amount.compareTo(other.amount) > 0; }

    private void assertSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new DomainException(String.format("币种不同,无法运算:%s vs %s", this.currency, other.currency));
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money that = (Money) o;
        return amount.equals(that.amount) && currency.equals(that.currency);
    }

    @Override
    public int hashCode() { return Objects.hash(amount, currency); }

    @Override
    public String toString() { return currency.getSymbol() + amount.toPlainString(); }
}

The constructor is private and factories enforce validation, guaranteeing immutability. All operations return a new Money instance, so the original object never changes.

Why Immutability Matters

A mutable value object leads to implicit sharing. The article shows a dangerous scenario where a shared Money reference is modified, silently corrupting the employee’s salary without any log. An immutable object eliminates this risk because any change requires creating a new instance.

Java 16+ record as a Lightweight Value Object

Records automatically generate constructor, getters, equals , hashCode , and toString , and are inherently immutable. Example: <code>public record Period(LocalDate start, LocalDate end) { public Period { if (start == null || end == null) throw new IllegalArgumentException("起止日期不能为 null"); if (end.isBefore(start)) throw new IllegalArgumentException(String.format("结束日期 %s 不能早于开始日期 %s", end, start)); } public boolean contains(LocalDate date) { return !date.isBefore(start) && !date.isAfter(end); } public long workDays() { return start.datesUntil(end.plusDays(1)).filter(d -> d.getDayOfWeek().getValue() < 6).count(); } } </code> Records are suitable for domain concepts such as Period , EmployeeId , PersonName , etc. When to Model a Concept as a Value Object The article lists a checklist: the concept is described by multiple attributes, has its own validation rules, and does not need independent tracking. A table of typical value objects in the payroll system (Money, EmployeeId, PersonName, IdCard, BankAccount, Period, SalaryLevel, TaxRate, Address, PayCycle) illustrates the rule. JPA Mapping Strategies for Value Objects Two approaches are described: Embedded mapping : the value object’s fields are inlined into the entity table using @Embedded and @AttributeOverrides . Example for MoneyVO and AddressVO shows column mapping. JSON serialization : the whole value object is stored as a JSON column via a custom AttributeConverter . This is convenient when the object has many fields or changes frequently, but prevents querying by sub‑fields. The choice depends on whether you need to query individual attributes (use @Embedded ) or only need whole‑object persistence (use JSON). Three Questions to Decide Entity vs Value Object Do you need to track the individual’s history? If yes → Entity. Can two instances with identical attributes be swapped without side effects? If no → Entity, if yes → Value Object. Does the concept have meaning outside its owning aggregate? If it does (e.g., Money ) → Value Object; if it only makes sense as part of another object (e.g., PaySlipLineItem ) → Entity (sub‑entity). Conclusion Entity = identity‑based, Value Object = attribute‑based and immutable. Understanding this distinction leads to correct equality implementation, proper use of equals / hashCode , and safer domain models. The next article will explore aggregate roots such as PaySlip and its line items.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaDDDDomain modelingentity()ImmutabilityJPAValue Object
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.