• Practical Advice
  • Value objects are one of the most underused tactical patterns in Domain-Driven Design. Most teams jump straight to entities and aggregates, but value objects are where the real expressiveness lives.

    What Makes a Value Object?

    A value object is defined by its attributes rather than by an identity. Two value objects with the same attributes are considered equal — there's no need for an id field.

    final readonly class Money
    {
        public function __construct(
            public int $amount,
            public Currency $currency,
        ) {
        }
    
        public function add(self $other): self
        {
            if (!$this->currency->equals($other->currency)) {
                throw new CurrencyMismatch($this->currency, $other->currency);
            }
    
            return new self($this->amount + $other->amount, $this->currency);
        }
    }
    

    Why They Matter

    Value objects push business rules into the type system itself. Instead of passing around primitive strings and integers — and scattering validation logic across your services — you encode constraints directly in the constructor.

    Self-Validating

    A EmailAddress value object that validates format on construction means you never have to wonder whether a string is actually a valid email. The type tells you.

    Immutable by Default

    PHP 8.2's readonly classes make value objects a natural fit. Once created, they can't be corrupted. Every operation returns a new instance.

    Rich Behavior

    Value objects aren't just data bags. They can carry domain behavior: Money::add(), DateRange::overlaps(), Address::format(). This is where your domain language comes alive.

    Practical Advice

    Start small. Look at your codebase for primitive obsession — places where you're passing string $email, int $cents, string $currency. Each of those is a value object waiting to happen.