Skip to main content

Patternbeginner

Value Object

Overview

A value object is defined by its attributes, not by identity. Two objects with the same values are interchangeable.

It is the tactical block with the best effort-to-return ratio — and the most underused.

Problem

Domain concepts represented by primitive types scatter responsibility and allow errors the compiler could have prevented.

void transfer(String source, String destination, BigDecimal amount)

Three problems in that signature.

Silent swap. Nothing prevents passing destination in place of source — they are the same type.

Scattered validation. Where do you check that the account is valid? In every caller, with variations.

Absent semantics. BigDecimal does not know that money has a currency, that you do not add amounts in different currencies, and that rounding follows a specific rule.

It is primitive obsession from the code smells catalogue, and value objects are the fix.

Core Concepts

Immutability is a requirement

A value object does not change. Operations return new objects:

salary.increasedBy(raise) → a new Money
period.extendedTo(date) → a new Period

That eliminates a whole class of defects: there is no way to alter a shared value by accident, and no need for defensive copying.

Valid by construction

The constructor validates. If a TaxId was created by the constructor, it is valid — and the caveat matters, because that is not the only path: ORMs and deserializers reconstitute objects by reflection, without going through it. That is how an invalid value gets into the domain.

That concentrates the validation in one place and makes it impossible to forget. Whoever receives a TaxId does not have to check anything.

The gain is greater than it looks: it eliminates the scattered defensive checking and the handling of "what if it is invalid?" in every consumer.

Behaviour belongs to the value

A value object is not just a wrapper around data. It carries the concept's operations.

Money knows to add only within the same currency, knows how to split with a defined rounding rule, knows how to compare. Period knows whether it contains a date, whether it overlaps another period, how many business days it has.

A value object with no behaviour is a wrapper with little return.

Where they appear in the domain

The candidates are recognizable: money, a quantity with a unit, a period, an interval, an address, a document, a code, a coordinate, a percentage, a range.

Practical rule: any concept the business names and that today is a primitive or a group of primitives that travel together.

The second case — data clumps — is the most frequent: startDate and endDate always passed together are a Period.

When to Use

  • A domain concept is represented by a primitive.
  • Several primitives always travel together.
  • There is validation repeated in several places.
  • There is a rule about the concept — rounding, comparison, formatting — scattered around.
  • Swapping two parameters of the same type is a possible error.

When Not to Use

When the concept has identity. It is an entity.

When there is neither behaviour nor validation. A pure wrapper over a string, with no rule at all, adds ceremony with no return. It is worth it when there is at least validation.

In generic or supporting subdomains, when the type has no rule of its own. The bar is higher outside the core: there, a proper name on its own does not pay for the wrapper. With validation or behaviour — a TaxId that validates itself — it pays off in any subdomain.

When the allocation shows up in the profile. A hot loop that creates one object per element, or a collection in the millions of instances, on a platform with no flattened value type. The condition is showing up in the profile — not looking like it would.

For transport data. An API DTO does not need internal value objects.

Alternatives

  • A primitive type with centralized validation — less safe, cheaper.
  • A lightweight nominal typenewtype, opaque type, a unit of measure: it gives type safety with no class. A transparent alias does not do the job: TypeScript's type, Kotlin's typealias and C's typedef are nicknames for the same type, and they let through any value of the right shape.
  • An immutable record — when there are grouped values and little behaviour.

Trade-offs

Value objectPrimitive
Valid by constructionValidation at every use
Parameter swap impossibleSilent
Behaviour next to the conceptScattered
Semantics explicit in the signatureAbsent
More types in the systemFewer
Conversion at the boundaryDirect

The last row is the real cost: value objects have to be converted when crossing the domain's boundary — for persistence, for the API. It is mapping work that primitives do not require.

Failure Modes

Mutable value object. It loses the guarantees and reintroduces the sharing risk.

Equality not implemented. Comparison by reference makes two equal values look different — and breaks collections and caches silently.

Anemic wrapper. With no validation and no behaviour.

Type explosion. A value object per field, including ones with no rule.

Leak into the API. The domain type at the external boundary ties the public contract to the internal model.

Common Mistakes

Not implementing equality and hash code. The most common defect and the subtlest.

Creating a wrapper with no rule.

Making it mutable.

Using it everywhere, including outside the core.

Real-World Example

A payroll system had BigDecimal for monetary values and double for percentages.

Two production defects, both traced to the absence of value objects.

The first: a holiday-pay proration rounded at each instalment, and the sum of the instalments differed from the total by cents. Multiplied by 4 thousand employees, it produced an accounting discrepancy that took a week to diagnose.

The second: a percentage was passed as 0.05 in one place and 5 in another — both double, no compilation error. The deduction came out 100 times larger for 12 employees.

Introducing Money and Percentage fixed both.

Money has split(int parts) which distributes the remainder deterministically — the last instalment absorbs the difference — and guarantees the sum of the parts is always the total. The rule ended up in one place, tested.

Percentage can only be constructed from a value with an explicit unit: Percentage.ofHundredths(5) or Percentage.ofFraction(0.05). The ambiguity disappeared from the signature.

Cost of the change: two weeks, including the mapping at the persistence boundary. In the three following years, no defect of the same category.

Practical Exercise

Look in your domain for parameters of the same primitive type that appear together in a signature — two Strings, two dates, two numbers.

Each pair is a possible silent swap, and a value object candidate.

Then look for validations that repeat: each one appearing in more than two places belongs to a value object.

Interview Questions

  • What characterizes a value object?
  • Why is immutability a requirement?
  • What is the risk of not implementing equality correctly?

Further Exploration

  • Evans, Eric. Domain-Driven Design. Addison-Wesley, 2003.
  • Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  • Fowler, Martin. ValueObject, 2016.
Finished reading this document?Your progress is saved in this browser only.