Skip to main content

Value Objects (VOs)

Key Concept

Value Objects represent immutable domain concepts by their values rather than their identity, enforcing business rules and maintaining data integrity.

Introduction

Value Objects (VOs) are a fundamental concept in Domain-Driven Design (DDD) that represent immutable domain concepts by their values rather than their identity. They are an essential tool for creating a rich domain model that enforces business rules and maintains data integrity.

Understanding Value Objects

There isn't a strict, universal definition for Value Objects as their usage depends heavily on the specific Context. However, we can define some core characteristics and purposes:

Value Objects

  • Represent data by value, not identity
  • Contain behavior relevant to their data
  • Always immutable

vs Data Transfer Objects

  • DTOs are data containers
  • No behavior in DTOs
  • Can be mutable or immutable

Layer Placement

  • Value Objects belong in the Domain Model layer
  • They should not appear in the Application or Presentation layers (e.g., Transformers)
  • An Entity can have one or more Value Objects

Purpose and Benefits

Why Use Value Objects?

Value Objects help ensure that:

  • Data type behavior remains consistent system-wide
  • Validations are applied uniformly
  • Structure remains consistent
  • Data integrity is maintained through immutability

Value Objects in Practice

Value Objects represent specific concepts in the system (like Email, Mobile, etc.). These are single pieces of data that have their own identity through their characteristics and validations.

In newer versions of Laravel, some features provide similar functionality to DDD Value Objects, though they are not exactly the same. However, they can be used to achieve similar goals in a Laravel context.

Core Characteristics

Immutability (Non-Negotiable)

Immutability is required - A Value Object's state cannot be changed after creation.

  • Once created, their state cannot be changed
  • Any modification must result in a new instance
  • Guarantees valid state throughout the object's lifetime

Value Equality

  • Compared by their attribute values
  • Two VOs with same values are considered equal
  • Example: Two Email VOs with "user@example.com" are equal

Implementing Value Equality

Value Objects must be comparable by their values. For example:

new Email('user@example.com') == new Email('user@example.com') // Returns true
class Email implements Stringable
{
private function __construct(private readonly string $value)
{
$this->validate($value);
}

public static function fromString(string $value): self
{
return new self($value);
}

// ... rest of the implementation

With these methods implemented, Value Objects can be properly compared and used in equality operations throughout the application.

Self-Validation

  • Value Objects validate their own state upon creation
  • They ensure they are always in a valid state
  • Invalid state is prevented through the constructor and factory methods
  • All validation rules are encapsulated within the Value Object itself

Contextual Nature

  • The use of Value Objects is highly context-dependent
  • The same concept might be a Value Object in one context and an Entity in another
  • The decision should be based on whether the object has an identity or is defined by its attributes

Implementation Guidelines

The Presence of Value Objects as an Indicator

  • The presence or absence of Value Objects in a system can be an indicator of design quality
  • A system with few or no Value Objects might suggest:
    • Weak implementation of Business Rules in the software layer
    • An anemic domain model
    • Business logic leaking into services or controllers
  • Conversely, a system with well-defined Value Objects typically indicates:
    • Stronger Business Rule contracts
    • More defensive programming practices
    • Better encapsulation of domain logic

Gradual Improvement

  • Implementing Value Objects where appropriate should be considered a form of continuous improvement
  • Existing codebases can be gradually refactored to introduce Value Objects
  • Each new Value Object makes the domain model richer and more expressive

Basic Structure

class Email implements Stringable
{
private function __construct(private string $value)
{
$this->validate($value);
}

public static function fromString(string $value): self
{
return new self($value);
}

private function validate(string $value): void
{
// Validation logic here
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email format");
}
}

public function __toString(): string
{
return $this->value;
}
}

When to Use Value Objects

warning

Not every business rule at the data level requires a Value Object. Value Objects should be used selectively for concepts that are repeated across different parts of the system and where type identification is important.

Value Objects are most beneficial in these scenarios:

  1. Repeated Business Rules

    • When the same validation rules and behaviors are needed in multiple parts of the system
    • For concepts that appear frequently in the domain (like Email, PhoneNumber, Money)
    • When consistent handling of a specific data type is critical across the application
  2. Domain Concepts

    • Represent domain concepts with specific rules (Email, PhoneNumber, Money, etc.)
    • Encapsulate validation and behavior that defines the concept's identity
    • Examples: Email, PhoneNumber, Address, Money, etc.
  3. Value Composition

    • Group related values that should be treated as a single unit
    • Example: Address containing street, city, postal code
    • Ensures these values are always used together and maintain their relationships
  4. Type Safety and Clarity

    • Prevent primitive obsession by creating specific types
    • Make method signatures more expressive and self-documenting
    • Example: public function register(Email $email, Password $password) is clearer than register(string $email, string $password)
  5. Business Rule Enforcement

    • When specific business rules must always be enforced for a particular piece of data
    • Ensures these rules cannot be bypassed by other parts of the system
    • Example: An OrderQuantity that must be positive and less than a maximum value
  6. Ubiquitous Language

    • To better reflect the domain language in your code
    • Makes the code more expressive and aligned with business terminology
    • Helps bridge the gap between domain experts and developers

Benefits

  • Improved Code Clarity: Expresses domain concepts explicitly
  • Reduced Duplication: Centralizes validation and behavior
  • Enhanced Type Safety: Prevents misuse of primitive types
  • Easier Testing: Self-contained units with clear boundaries

Common Patterns

Factory Methods

public static function fromString(string $value): self
{
return new self($value);
}

// Usage
$email = Email::fromString('user@example.com');

Value Object Collections

class EmailCollection extends Collection
{
public function __construct(Email ...$emails)
{
parent::__construct($emails);
}

public function contains(Email $email): bool
{
return $this->contains(fn (Email $e) => $e->equals($email));
}
}

Best Practices

  1. Keep It Focused

    • Each Value Object should have a single responsibility
    • Avoid complex domain logic that belongs in Domain Services
    • Keep the scope narrow and well-defined
  2. Strict Immutability

    • Always return new instances for modifications
    • Make all properties private and readonly
    • Prevent any method from modifying internal state after construction
    • This ensures thread safety and predictable behavior
  3. Comprehensive Validation

    • Validate all input in the constructor
    • Throw meaningful, domain-specific exceptions for invalid data
    • Consider using a factory method pattern for complex validations
    • Document the validation rules in the class docblock
  4. Layer Awareness

    • Keep Value Objects in the Domain Model layer
    • Don't let them leak into Application or Presentation layers
    • Use DTOs or other patterns for data transfer between layers
    • This maintains layer isolation and architectural boundaries
  5. Laravel Integration

    • Use custom casts for seamless database interaction
    • Implement Arrayable and Jsonable when needed
    • Consider using value object collections for handling multiple values
    • Be mindful of serialization/deserialization when using Laravel's caching
  6. Testing Considerations

    • Test edge cases in validation rules
    • Verify immutability in tests
    • Test value equality semantics
    • Consider property-based testing for complex value objects

Example: Email Value Object

class Email implements Stringable
{
private function __construct(private string $value)
{
$this->validate();
}

public static function fromString(string $value): self
{
return new self($value);
}

private function validate(): void
{
if (!filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException("The email '{$this->value}' is not a valid email address.");
}

// Additional business rules could be added here
// For example, checking for disposable email domains
}

public function getDomain(): string
{
return explode('@', $this->value, 2)[1] ?? '';
}

public function equals(self $other): bool
{
return strtolower($this->value) === strtolower($other->value);
}

public function __toString(): string
{
return $this->value;
}
}

Example: Money Value Object

class Money
{
public function __construct(
private readonly float $amount,
private readonly string $currency
) {
$this->validate();
}

private function validate(): void
{
if ($this->amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}

if (!in_array($this->currency, ['USD', 'EUR', 'GBP'], true)) {
throw new InvalidArgumentException('Unsupported currency');
}
}

public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currencies must match');
}

return new self($this->amount + $other->amount, $this->currency);
}

// Other methods...
}

Conclusion

Value Objects are a powerful tool for creating a rich, expressive domain model. By encapsulating validation and behavior, they help maintain data integrity and make the codebase more maintainable and self-documenting. Start identifying primitive values in your domain that could benefit from being converted into Value Objects to make your code more robust and expressive.