When thinking of DDD’s tactical patterns, many people start with Entity. Objects with identifiers, lifecycles, and state changes are certainly important. But in real code, the smallest and clearest starting point for revealing domain meaning is often a Value Object.
A Value Object is not simply a small object. A Value Object is a way to lift domain meaning scattered across primitive types into a named concept.
Having many int, String, and LocalDateTime values in code is not itself a problem. The problem appears when it is hard to know what domain meaning those values have just by reading the code.
Primitive types hide too much
Suppose a subscription service has code like this:
public class Subscription {
private int price;
private String currency;
private int periodDays;
private String email;
}
At first, this looks natural. Price is a number, currency is a string, period is a number of days, and email is also a string. But many questions are hidden inside this code.
Can price be negative?
What format should currency have?
What does it mean when periodDays is 0?
Is email a validated value?
Should price and currency always move together?
If the answers to these questions are not visible in code, validation logic scatters across many places.
if (price < 0) {
throw new IllegalArgumentException();
}
if (!currency.equals("KRW") && !currency.equals("USD")) {
throw new IllegalArgumentException();
}
This code can work. But it does not preserve domain meaning well. It also weakly expresses the fact that price and currency together form one amount of money.
Value Object turns this meaning into named objects.
Money monthlyFee;
SubscriptionPeriod period;
EmailAddress emailAddress;
Now the code speaks in domain concepts, not just simple values.
Value Objects are identified by value
The biggest difference between Entity and Value Object is identity. Entity is distinguished by an identifier. Even if two subscriptions have the same plan and the same expiration date, they are different subscriptions if their identifiers differ.
Value Object is different. A Value Object is considered the same if its values are the same.
Money a = new Money(10000, Currency.KRW);
Money b = new Money(10000, Currency.KRW);
These may be different instances, but in the domain they are the same value. 10,000 won is 10,000 won. It does not need a separate identifier.
Because of this characteristic, Value Objects are usually immutable. When a value created once does not change, comparison and reasoning become easier.
public record Money(long amount, Currency currency) {
public Money {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative.");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Different currencies cannot be added.");
}
return new Money(this.amount + other.amount, this.currency);
}
}
What matters here is that Money is not just a data bundle. Money has domain rules about money. The rules that the amount cannot be negative and that different currencies cannot be added are inside the value object.
Immutability is stability, not restriction
Making Value Objects immutable may look inconvenient at first, because changing a value requires creating a new object.
Money fee = new Money(10000, KRW);
Money discounted = fee.subtract(new Money(1000, KRW));
But this inconvenience becomes an advantage. We gain confidence that the value will not change midway. Especially as domain logic becomes more complex, immutability becomes powerful.
For example, suppose there is a SubscriptionPeriod that represents a subscription period.
public record SubscriptionPeriod(LocalDate startDate, LocalDate endDate) {
public SubscriptionPeriod {
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("End date cannot be before start date.");
}
}
public boolean contains(LocalDate date) {
return !date.isBefore(startDate) && !date.isAfter(endDate);
}
}
If this object is immutable, a strange state where “only the start date changed while the end date remained the same” cannot appear. Only valid periods can exist.
One important role of Value Object is making it impossible to create invalid values in the first place.
Escaping Primitive Obsession
The excessive use of primitive types is called Primitive Obsession. Expressing everything with String, int, and long makes code look simple, but domain meaning disappears.
Look at this method:
public void changePlan(Long subscriptionId, String planCode, int amount, int days) {
// ...
}
What does this method receive? Is amount the payment amount, discount amount, or monthly fee? Are days the subscription period, grace period, or free trial period? Is planCode a valid plan code?
With Value Objects, the intent becomes much clearer.
public void changePlan(
SubscriptionId subscriptionId,
PlanCode targetPlanCode,
Money changeFee,
SubscriptionPeriod nextPeriod
) {
// ...
}
The code became a little longer, but the reader guesses less. The purpose of good code is not to be short. It is to avoid losing important meaning.
Value Objects can have behavior
It is unfortunate to make Value Objects into objects with only getters. A Value Object can have behavior related to its value.
For example, EmailAddress can validate email format. Money can add, subtract, and compare. PlanQuota can determine whether usage has exceeded the limit.
public record UsageLimit(long maxUsage) {
public UsageLimit {
if (maxUsage < 0) {
throw new IllegalArgumentException("Usage limit cannot be negative.");
}
}
public boolean isExceededBy(long currentUsage) {
return currentUsage > maxUsage;
}
}
Without this behavior, the same condition repeats in many places.
if (currentUsage > plan.getMaxUsage()) {
// limit
}
When behavior is placed in the Value Object, domain meaning gathers in one place.
if (plan.usageLimit().isExceededBy(currentUsage)) {
// limit
}
It is a small difference, but the code becomes closer to the language of the domain.
When should we create a Value Object?
We do not need to wrap every primitive value in a Value Object. Doing so can make the code unnecessarily heavy. What matters is whether the value has domain meaning or rules.
We can ask these questions:
Does this value have validation rules?
Are calculations or comparisons related to this value repeated?
Does this value always move together with several fields?
Would using this value incorrectly cause an important bug?
Does this value have a name in the domain?
If the answer to several of these questions is “yes,” a Value Object is worth considering.
Concepts such as Money, EmailAddress, SubscriptionPeriod, PlanCode, UsageLimit, and SeatCount can be good candidates.
On the other hand, primitive types may be enough for values with small meaning and almost no rules, such as simple display order or internal flags.
Closing
Value Object is not an argument for creating many small objects. Value Object is a proposal to express important values in the domain as named concepts.
Primitive types are convenient. But primitive types hide domain meaning. If concepts such as amount, period, email, plan code, and usage limit are expressed only as int and String, validation and rules scatter across the code.
Value Object gathers that meaning in one place. It makes invalid values impossible to create, puts behavior related to the value inside the value itself, and above all makes the code speak the language of the domain.
DDD does not start only from large architecture. Sometimes it starts from a small choice such as changing String email to EmailAddress. That small choice can become the first step toward leaving domain meaning inside the code.