Value objects for domain modeling

Sarah Mitchell Feb 2026
2 tabs
class Money
  include Comparable

  attr_reader :amount, :currency

  def initialize(amount, currency = 'USD')
    @amount = BigDecimal(amount.to_s)
    @currency = currency.upcase
    freeze  # Make immutable
  end

  def +(other)
    ensure_same_currency!(other)
    Money.new(amount + other.amount, currency)
  end

  def -(other)
    ensure_same_currency!(other)
    Money.new(amount - other.amount, currency)
  end

  def *(multiplier)
    Money.new(amount * multiplier, currency)
  end

  def /(divisor)
    Money.new(amount / divisor, currency)
  end

  def <=>(other)
    ensure_same_currency!(other)
    amount <=> other.amount
  end

  def ==(other)
    other.is_a?(Money) &&
      amount == other.amount &&
      currency == other.currency
  end
  alias eql? ==

  def hash
    [amount, currency].hash
  end

  def to_s
    "#{currency_symbol}#{formatted_amount}"
  end

  def to_f
    amount.to_f
  end

  def zero?
    amount.zero?
  end

  def positive?
    amount.positive?
  end

  def negative?
    amount.negative?
  end

  private

  def ensure_same_currency!(other)
    return if currency == other.currency

    raise ArgumentError,
      "Cannot operate on different currencies: #{currency} and #{other.currency}"
  end

  def currency_symbol
    { 'USD' => '$', 'EUR' => '€', 'GBP' => '£' }[currency] || currency
  end

  def formatted_amount
    format('%.2f', amount)
  end
end

# Usage
price = Money.new(19.99, 'USD')
tax = Money.new(2.50, 'USD')
total = price + tax
# => $22.49

discount = total * 0.10
final_price = total - discount

# Comparison
price > tax  # => true
[price, tax, discount].sort  # => sorted by amount

# Equality
Money.new(10, 'USD') == Money.new(10, 'USD')  # => true
Money.new(10, 'USD') == Money.new(10, 'EUR')  # => false

# Using in models
class Order < ApplicationRecord
  def total_price
    Money.new(total_cents / 100.0, 'USD')
  end

  def total_price=(money)
    self.total_cents = (money.amount * 100).to_i
  end
end

order = Order.new
order.total_price = Money.new(99.99)
order.total_price  # => $99.99
2 files · ruby Explain with highlit

Value objects represent immutable domain concepts without identity. I use value objects for money, addresses, date ranges, coordinates. Value objects are compared by value, not identity—two identical addresses are equal. They're immutable—create new instances rather than modifying. Value objects encapsulate validation and behavior. Including Comparable enables sorting and comparison. Value objects reduce primitive obsession—Money instead of decimals, Address instead of strings. They make code expressive and type-safe. Testing value objects is straightforward—pure Ruby without dependencies. Understanding when to use value objects vs. entities improves domain modeling. Value objects are fundamental to Domain-Driven Design.