Concept · Architecture & domain modeling

Value objects

Small immutable types defined by their values — Money, Email, DateRange — that validate themselves and replace error-prone primitives.

2 minprimitive obsessionDDDimmutable typesbranded types

A value object is a type defined entirely by its attributes, with no identity of its own. Two Money values of 25,000 IDR are interchangeable, just as two copies of the number 7 are. Contrast an entity, such as a customer, which keeps its identity while its attributes change.

Properties

  • Equality by value: compare all fields, not identity.
  • Immutable: operations return new values (a.Add(b) returns a new Money) instead of mutating.
  • Always valid: a constructor validates input, so an invalid instance can't exist.
  • Behaviour lives with the data: formatting, arithmetic, comparisons and conversions belong on the type.
go
type Email struct{ value string }

func ParseEmail(s string) (Email, error) {
	addr, err := mail.ParseAddress(s)
	if err != nil || addr.Name != "" {
		return Email{}, fmt.Errorf("invalid email %q", s)
	}
	return Email{value: strings.ToLower(addr.Address)}, nil
}

func (e Email) String() string { return e.value }

In Go, unexported fields plus a constructor enforce validity, and comparable structs give value equality with == for free. In TypeScript, use readonly fields or a branded type (type Email = string & { readonly __brand: 'Email' }) produced only by a validating function.

Why use them

Replacing primitives — the primitive obsession smell — removes whole classes of bugs:

  • Money stored as integer minor units with a currency prevents float rounding errors and adding rupiah to ringgit.
  • Email, PhoneNumber and OrderID types stop arguments being passed in the wrong order, because a function taking (CustomerID, OrderID) won't accept them swapped.
  • DateRange puts "does this range contain that time?" and "do these overlap?" in one tested place, ending inclusive/exclusive boundary bugs.
  • Validation happens once, at the edge, instead of defensively everywhere.

Good candidates: money, quantities, percentages, coordinates, addresses, date ranges, identifiers, and any string with a format. Value objects are the building blocks of aggregates in domain-driven design, and a common target in the refactoring lesson.

Learn it in depth