The Architecture Reference

Ddd tactical · Domain-Driven Design · Beginner

Value Objects

Immutable types identified by the composition of their values — data plus behavior that cure primitive obsession.

Ddd tactical Beginner ⏱ 4 min read Complete

🧭 Analogy

A $10 bill is interchangeable with any other $10 bill — you don’t care which note you hold, only its value. Contrast that with your passport, which is uniquely yours. Value objects are the $10 bills of your model: identified entirely by what they are, not by a serial number.

Identified by value

A value object is an object identified by the composition of its values; it needs no explicit identity field. Because its identity is its values, it is immutable — any operation that would change a value returns a new instance instead. A value object models both data and behavior, centralizes the logic that manipulates it, overrides equality to compare by value, and speaks the ubiquitous language.

graph TD
P["Person (entity)"] --> Id["PersonId (value object)"]
P --> Name["Name (value object)"]
P --> Phone["PhoneNumber (value object)"]
P --> Email["EmailAddress (value object)"]
P --> H["Height (value object)"]
Phone --> V["Validates + parses on construction"]
H --> Beh["Behavior: Metric(180), unit conversion"]

Curing primitive obsession

Primitive obsession is the smell of representing domain concepts with primitive types — a phone number as a string, a height as an int. The validation and conversion logic then scatters across the codebase, gets duplicated, and drifts out of sync. A value object fixes this: it validates on construction, so an invalid instance can never exist, and it hosts its own behavior.

graph TD
subgraph Prim["Primitive obsession"]
  S["phone: string"] --> V1["validate in controller"]
  S --> V2["validate in service"]
  S --> V3["re-validate in repo (drifts)"]
end
subgraph VO["Value object"]
  PN["PhoneNumber"] --> One["validate + parse once,<br/>on construction"]
end

A Person built from all-string primitives becomes far richer when rebuilt from value objects: PersonId, Name, PhoneNumber, EmailAddress, and Height, each with rich behavior — Height.Metric(180), PhoneNumber.Parse(...), Color.FromRGB(...).MixWith(green).

Color(red=255, green=0, blue=0)         // identified by its three values
Color.FromRGB(255,0,0) == Color.FromRGB(255,0,0)   // true — value equality
Color.FromRGB(255,0,0).MixWith(green)   // returns a NEW Color

Use them whenever you can

Reach for value objects for almost every property of other objects — especially money, where bare floats invite precision and rounding disasters. Some languages make this nearly free: a C# 9 record gives value-based equality out of the box.

Behavior belongs with the data

A value object keeps all the logic for manipulating a concept inside its boundary. That prevents the logic from leaking and duplicating in the application layer — the same principle that makes aggregates and the whole domain model effective.

Don't add a redundant identity

A bug source is giving a value object an identity it doesn’t need — e.g. a ColorId on a Color. If two instances with identical values should be considered the same thing, it’s a value object; adding an ID turns it into something it isn’t and breaks value equality.

See also

When to use it — and when not

✅ Reach for it when

  • For properties of other objects — IDs, names, phone numbers, emails, colors, statuses.
  • Especially for money, where primitive types invite precision and rounding bugs.
  • Whenever validation or manipulation logic for a concept is scattered and duplicated.

⛔ Think twice when

  • When a concept genuinely needs a distinct identity that persists through changes — use an entity instead.
  • Don't over-model trivial supporting-subdomain data into elaborate value objects when CRUD suffices.

Check your understanding

Score: 0 / 3

1. What identifies a value object?

A value object is identified by the combination of all its values; two value objects with the same values are equal, so no identity field is required.

2. Why are value objects immutable?

Since a value object is its values, any 'mutation' is conceptually a different value — so mutating operations return a new instance rather than changing the existing one.

3. What code smell do value objects cure?

Primitive obsession scatters and duplicates validation across the codebase; a value object validates on construction and centralizes its manipulation logic in one place.

Comments

Sign in with GitHub to join the discussion.