# Value Object Pattern ## Problem Primitive types (strings, numbers) are frequently used to represent domain concepts like money, email addresses, or coordinates. But primitives carry no validation, no behavior, and no semantic meaning — `$10 USD` is just a float, and mixing currencies or comparing incompatible values causes bugs that the type system can't catch. ## Solution Create small, **immutable** objects that represent domain concepts by **value** rather than identity. Two value objects are equal if all their attributes are equal (unlike entities, which are equal by ID). Value objects encapsulate validation and domain-specific behavior. Key properties: - **Immutable** — Once created, never changed. - **Equality by value** — Two `Money(10, "USD")` instances are equal. - **Self-validating** — Invalid states are rejected at construction. - **Side-effect free** — Operations return new instances. ## When to Use - Money/currency amounts - Email addresses, phone numbers, URLs - Geographic coordinates, measurements - Date ranges, time periods - Anywhere primitives carry implicit constraints ## When to Avoid - When identity matters (use Entity instead) - When mutability is genuinely required - When the concept is truly a single primitive with no validation needs ## Pseudocode ``` class Money: amount: decimal (immutable) currency: string (immutable) function equals(other): return amount == other.amount AND currency == other.currency function add(other): assert currency == other.currency return new Money(amount + other.amount, currency) ``` ## Python Implementation ```python from dataclasses import dataclass @dataclass(frozen=True) class Money: amount: float currency: str def __post_init__(self): if self.amount < 0: raise ValueError(f"Amount cannot be negative: {self.amount}") if len(self.currency) != 3: raise ValueError(f"Currency must be 3-letter code: {self.currency}") def add(self, other: "Money") -> "Money": if self.currency != other.currency: raise ValueError(f"Cannot add {self.currency} and {other.currency}") return Money(round(self.amount + other.amount, 2), self.currency) def subtract(self, other: "Money") -> "Money": if self.currency != other.currency: raise ValueError(f"Cannot subtract {self.currency} and {other.currency}") result = round(self.amount - other.amount, 2) if result < 0: raise ValueError("Result would be negative") return Money(result, self.currency) def multiply(self, factor: float) -> "Money": return Money(round(self.amount * factor, 2), self.currency) def __str__(self): return f"{self.currency} {self.amount:.2f}" @dataclass(frozen=True) class Email: address: str def __post_init__(self): if "@" not in self.address or "." not in self.address.split("@")[-1]: raise ValueError(f"Invalid email: {self.address}") # Normalize to lowercase object.__setattr__(self, "address", self.address.lower()) def domain(self) -> str: return self.address.split("@")[1] def __str__(self): return self.address # --- Demo --- print("=== Money value object ===") price = Money(29.99, "USD") tax = Money(2.40, "USD") total = price.add(tax) print(f" {price} + {tax} = {total}") discounted = total.multiply(0.9) print(f" 10% discount: {discounted}") # Equality by value a = Money(10.00, "USD") b = Money(10.00, "USD") c = Money(10.00, "EUR") print(f" Money(10, USD) == Money(10, USD)? {a == b}") print(f" Money(10, USD) == Money(10, EUR)? {a == c}") # Can be used in sets/dicts (hashable because frozen) prices = {Money(9.99, "USD"), Money(9.99, "USD"), Money(19.99, "USD")} print(f" Unique prices: {len(prices)}") print() print("=== Email value object ===") email = Email("Alice@Example.COM") print(f" Normalized: {email}") print(f" Domain: {email.domain()}") print(f" Equal to lowercase? {email == Email('alice@example.com')}") print() print("=== Validation ===") try: Money(-5, "USD") except ValueError as e: print(f" Negative amount: {e}") try: Money(10, "USD").add(Money(5, "EUR")) except ValueError as e: print(f" Mixed currency: {e}") try: Email("not-an-email") except ValueError as e: print(f" Bad email: {e}") ``` **Output:** ``` === Money value object === USD 29.99 + USD 2.40 = USD 32.39 10% discount: USD 29.15 Money(10, USD) == Money(10, USD)? True Money(10, USD) == Money(10, EUR)? False Unique prices: 2 === Email value object === Normalized: alice@example.com Domain: example.com Equal to lowercase? True === Validation === Negative amount: Amount cannot be negative: -5 Mixed currency: Cannot add USD and EUR Bad email: Invalid email: not-an-email ``` ## Go Implementation ```go package main import ( "fmt" "math" "strings" ) type Money struct { Amount float64 Currency string } func NewMoney(amount float64, currency string) (Money, error) { if amount < 0 { return Money{}, fmt.Errorf("amount cannot be negative: %f", amount) } if len(currency) != 3 { return Money{}, fmt.Errorf("currency must be 3-letter code: %s", currency) } return Money{Amount: math.Round(amount*100) / 100, Currency: currency}, nil } func (m Money) Add(other Money) (Money, error) { if m.Currency != other.Currency { return Money{}, fmt.Errorf("cannot add %s and %s", m.Currency, other.Currency) } return NewMoney(m.Amount+other.Amount, m.Currency) } func (m Money) Multiply(factor float64) Money { result, _ := NewMoney(m.Amount*factor, m.Currency) return result } func (m Money) Equal(other Money) bool { return m.Amount == other.Amount && m.Currency == other.Currency } func (m Money) String() string { return fmt.Sprintf("%s %.2f", m.Currency, m.Amount) } type Email struct { Address string } func NewEmail(address string) (Email, error) { lower := strings.ToLower(address) parts := strings.SplitN(lower, "@", 2) if len(parts) != 2 || !strings.Contains(parts[1], ".") { return Email{}, fmt.Errorf("invalid email: %s", address) } return Email{Address: lower}, nil } func main() { fmt.Println("=== Money value object ===") price, _ := NewMoney(29.99, "USD") tax, _ := NewMoney(2.40, "USD") total, _ := price.Add(tax) fmt.Printf(" %s + %s = %s\n", price, tax, total) discounted := total.Multiply(0.9) fmt.Printf(" 10%% discount: %s\n", discounted) a, _ := NewMoney(10.00, "USD") b, _ := NewMoney(10.00, "USD") fmt.Printf(" Equal? %v\n", a.Equal(b)) fmt.Println() fmt.Println("=== Email value object ===") email, _ := NewEmail("Alice@Example.COM") fmt.Printf(" Normalized: %s\n", email.Address) fmt.Println() fmt.Println("=== Validation ===") _, err := NewMoney(-5, "USD") fmt.Printf(" Negative: %v\n", err) _, err = NewEmail("not-an-email") fmt.Printf(" Bad email: %v\n", err) } ``` ## JavaScript Implementation ```javascript class Money { #amount; #currency; constructor(amount, currency) { if (amount < 0) throw new Error(`Amount cannot be negative: ${amount}`); if (currency.length !== 3) throw new Error(`Currency must be 3-letter code: ${currency}`); this.#amount = Math.round(amount * 100) / 100; this.#currency = currency; Object.freeze(this); } get amount() { return this.#amount; } get currency() { return this.#currency; } add(other) { if (this.#currency !== other.currency) { throw new Error(`Cannot add ${this.#currency} and ${other.currency}`); } return new Money(this.#amount + other.amount, this.#currency); } multiply(factor) { return new Money(this.#amount * factor, this.#currency); } equals(other) { return this.#amount === other.amount && this.#currency === other.currency; } toString() { return `${this.#currency} ${this.#amount.toFixed(2)}`; } } // --- Demo --- console.log("=== Money value object ==="); const price = new Money(29.99, "USD"); const tax = new Money(2.40, "USD"); const total = price.add(tax); console.log(` ${price} + ${tax} = ${total}`); console.log(` 10% discount: ${total.multiply(0.9)}`); const a = new Money(10, "USD"); const b = new Money(10, "USD"); console.log(` Equal? ${a.equals(b)}`); console.log("\n=== Validation ==="); try { new Money(-5, "USD"); } catch (e) { console.log(` Negative: ${e.message}`); } try { new Money(10, "USD").add(new Money(5, "EUR")); } catch (e) { console.log(` Mixed: ${e.message}`); } ``` ## Related Patterns - **Immutability** — Value Objects must be immutable. Immutability is the key property that makes Value Objects safe to share. - **Flyweight** — When the same Value Object instance is shared across many places, Flyweight manages that sharing efficiently. - **Guard Clause** — Value Object constructors use Guard Clause to validate and reject invalid values at creation time. - **Aggregate** — Value Objects live inside Aggregates (as properties of entities). They are part of the Aggregate but are not entities. - **DTO** — A DTO transfers data; a Value Object models a domain concept. A Value Object can be a field type inside a DTO.