Building Resilient Code: Harnessing the Power of Value Objects

Value objects are a very effective tool to improve the quality of your application, as well as the readability of your code. These are small objects which contain domain-specific business rules. They're particularly useful when working with dynamically-typed languages like PHP, where the move to using more structured types has been pivotal in improving application quality.

What are value objects, and why do we need them?

Value objects are small objects which provide a guarantee of validity on the data they contain. They're immutable representations of data, which are aware of business rules. They help us to write more robust and readable code, minimising bugs. They're sometimes confused with Data Transfer Objects, which are another useful tool for ensuring data consistency within an application. At a high level, DTOs are concerned with the structure of data, while value objects are concerned with the validity of that data. We'll go into more detail on the difference between value objects and DTOs a little later on!

That's quite a mouthful, and we already have type-checking in most modern languages, so what's the benefit of adding a new concept to our stack? Perhaps an example will make it clearer!

Consider the following example, where we are trying to create a user.

class User {
  public function __construct(
    private string $name,
    private string $email,
    private string $phone
  ) {}
}

$user = new User('Paul', 'Conroy', 'paul@conroyp.com');

In this case, we have type checking on our parameters, but we've also got a bug sneaking in here. We may have been coding distracted or half-asleep, and thought our constructor wanted first name, last name, email. Now our $email contains what looks like a last name, and our $phone has an email address in it. And this, despite complying with all of the type restrictions. Oops!

// Result:
User {
  -name: "Paul",
  -email: "Conroy",
  -phone: "paul@conroyp.com",
}

This type of bug can easily happen all over an application. Value objects can help us by putting a more formal definition on the values we work with, complaining loudly if we're not complying with the rules. This means that rather than dealing with a string which (we hope!) contains an email address, we have an Email value object. Instead of a pair of latitude and longitude strings, we deal with a GeoPoint, and so on.

Sounds like a lot of overhead for a relatively simple use case. Do we really need them?

Let's take an example of an application where we need to handle prices. In an application without value objects, our code may be full of functions which have to pass a combination of number (amount) and string (currency) around. Every function which receives these values needs to repeat validation on them to avoid errors sneaking in. Skimping on these checks makes our code look cleaner, but risks introducing bugs. This is where value objects shine.

enum Currency : string
{
  case Dollar = 'USD';
  case Euro = 'EUR';
}

// Immutable object, so let's make it readonly!
readonly class Price implements Stringable
{
  public function __construct(
    private float $amount,
    private Currency $currency)
  {
    // Run validation check
    if ($this->amount <= 0) {
      throw new InvalidPriceAmountException();
    }
  }

  public function __toString(): string
  {
    return number_format($this->amount, 2) . ' ' . $this->currency->value;
  }

  public function getAmount(): float
  {
    return $this->amount;
  }

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

$price = new Price(15, ‘$’); // Invalid Argument Exception
$price = new Price(0, Currency::Dollar); // InvalidPriceAmountException

$price = new Price(29.99, Currency::Dollar); // OK
echo $price; // 29.99 USD

Whenever we want to represent a price in our code, we can create a price object, and trust the internal validation done on instantiation. This means that for the lifecycle of that object in our code, we don't need to revalidate it everywhere this value is passed between functions.

// Before: requires validation every time we pass around an amount and currency

class UserBalance
{
  ...

  function addToBalance(float $amount, string $currency) : void
  {
    // Validation before we can trust the values we
    // received are "business-usable"
    if (!currencyIsValid($currency)) {
      throw new InvalidCurrencyException;
    }

    if ($amount <= 0) {
      throw new InvalidAmountException;
    }

    $this->balance += $amount;
  }
}

class Cart
{
  function removeFromTotal(float $amount, string $currency) : void
  {
    // Validation needed again
    if (!currencyIsValid($currency)) {
      throw new InvalidCurrencyException;
    }

    if ($amount <= 0) {
      throw new InvalidAmountException;
    }

    $this->total -= $amount;
  }
}

...
$userBalance->addToBalance(59.99, Currency::Dollar);
$cart->removeFromTotal(19.99, Currency::Dollar);
// After: Relies on the Price value object

class UserBalance
{
  ...

  function addToBalance(Price $price) : void
  {
    $this->balance += $price->getAmount();
  }
}

class Cart
{
  function removeFromTotal(Price $price, float $discount) : void
  {
    $this->total -= $price->getAmount();
  }
}

...
$price = new Price(59.99, Currency::Dollar);
$userBalance->addToBalance($price);

$cart->removeFromTotal(new Price(19.99, Currency::Dollar));

This is a fairly trivial example - many more useful cases exist:

  • Geo location: No more mixing up whether lat or long goes first into the 30+ different functions in your code which are concerned with positioning!
  • Email address: Ensuring that strings are correctly-formatted email addresses. We could also add domain-specific blocking or allowing of certain domains. Early Facebook only allowed college domains, so in the example, an email value object could throw an exception for any domain not on the approved list.
  • Phone numbers: Dealing with formatting, normalisation of spaces, and returning a consistent representation.
  • Address: Handling city, state, country, zip code, and other regional levels. Handling validation, missing data, etc - a great way to centralise all the falsehoods programmers believe about addresses in one place!

Equality checks

What if we want to check if two value objects are the same? We can implement custom equality checks.

readonly class Price
{
  ...
  public function isEqualTo(Price $other) : bool
  {
    return $this->getAmount() == $other->getAmount()
      && $this->getCurrency() == $other->getCurrency();
  }
}

This gives us the flexibility to decide what exactly makes two objects equal.

Let's take a case where we have Person objects, with values for name, date of birth, country of birth, and favourite colour. Perhaps people change their favourite colour frequently, so we want to exclude it from our custom check.

readonly class Person
{
  public function __construct(
    private string $name,
    private DateTime $dob,
    private string $country,
    private string $favouriteColour)
  ...

  public function isEqualTo(Person $other) : bool
  {
    // Ignore $favouriteColour from equality checks
    return $this->getName() == $other->getName()
      && $this->getDob() == $other->getDob()
      && $this->getCountry() == $other->getCountry();
  }
}

$first = new Person('Paul', new DateTime('2020-...'), 'Ireland', 'red');
$second = new Person('Paul', new DateTime('2020-...'), 'Ireland', 'green');

$first->isEqualTo($second); // true!

Or perhaps we are capturing a lot of person data from mobile phones, where capitalisation can be erratic. We want to treat paul and Paul the same, so need to lowercase the names before comparison.

readonly class Person
{
    ...
    public function isEqualTo(Person $other) : bool
    {
      // Ignore case differences
      return strtolower($this->getName()) == strtolower($other->getName())
        && ...
    }
}

$first = new Person('paul', new DateTime('2...'
$second = new Person('Paul', new DateTime('2...'

$first->isEqualTo($second); // true!

Additional functionality

Value objects generally have mostly getter-type functions, but can also house some slightly more complex logic. We've seen the equals check above, and can look at a more complex example, with a DateRange value object. This object is set up to represent the start and end of a range of dates in our application. Perhaps it's an art gallery application, and our DateRange object is capturing the start and end dates of a particular exhibition. When a user wants to book tickets, it would be useful to know if their chosen date is during the exhibition.

readonly class DateRange
{
    public function __construct(
        private DateTime $start,
        private DateTime $end
    ) {}

    ...
    public function contains(DateTime $selected) : bool
    {
      return $selected >= $start && $selected <= $end;
    }
}

$exhibition = new DateRange($startDate, $endDate);
$exhibition->contains($selectedDate);

What about nested value objects?

I put a value object in your value object
Nothing says "contemporary article" quite like a 15 year old meme reference

As the attributes on a value object are typed, it is possible to have value objects which contain other value objects. Consider the case of an employee, who has a name, email, and a salary. There are rules about the salary - it'll have a currency, an amount, it'll (hopefully!) be greater than zero, and may even have a bonus element. We can add business logic validation for that into our employee value object, but if we're creating salary value objects elsewhere in the application, maybe we want to extract it to it's own value object.

readonly class Salary
{
  public function __construct(
    private float $amount,
    private string $currency,
    private float $bonus
  ) {
    // Validation...
  }
}

readonly class Employee
{
  public function __construct(
    private string $name,
    private string $email,
    // Using the Salary value object directly
    private Salary $salary)
  ...
}

$employee = new Employee('Paul', 'home@work.com', new Salary(24, 'USD', 0));

Maintenance advantages

The user of value objects within a codebase can significantly reduce the maintenance required on an application. Let's consider a scenario where our application has to handle multi-part address information, in multiple places in the codebase.

public function saveAddress($street, $city, $zipcode, $country)
...
public function sendAddressToPrint($street, $city, $zipcode, $country)
...
public function calculatePostalCostsForAddress($street, $city, $zipcode, $country)
...
// and so on...

Now let's say that there's a business requirement to change the way that addresses are represented internally. Maybe we're breaking out the house number separate to the $street, or maybe there's a new $sublocality between $street and $city levels. If we have to add in awareness of this new field, we'll have to change all of the function definitions. If these functions are currently implementing a specific interface, we'll have to make sure that is updated also, along with any other implementations. It can very quickly get very messy.

If we're already using a value object for our address, our change is a bit easier. We modify the definition of the Address value object, but if each address-related function already accepts an Address, we don't need to modify any of those definitions.

// Old
$address = new Address('10 Downing Street', 'London', 'SW1A 2AA', 'UK');
// New
$address = new Address('10', 'Downing Street', 'London', 'SW1A 2AA', 'UK');

public function saveAddress(Address $address)
...
public function sendAddressToPrint(Address $address)
...
public function calculatePostalCostsForAddress(Address $address)
...

When should I use a value object instead of a data transfer object?

Data Transfer Objects (DTOs) are great for sharing data between layers of an application. They define a schema that we can reliably expect to work with, as data enters or leaves our application. The data being passed is often mutable. However, DTOs are concerned only with the data types which appear in the data, rather than the business logic involved.

From wikipedia:

A DTO does not have any behavior except for storage, retrieval, serialization and deserialization of its own data (mutators, accessors, parsers and serializers). In other words, DTOs are simple objects that should not contain any business logic but may contain serialization and deserialization mechanisms for transferring data over the wire.
class EmployeeDTO
{
    public function __construct(
      private string $name,
      private string $phone,
      private string $email,
      private string $salaryCurrency,
      private float $salaryAmount,
      ....
    )
}

Value objects, on the other hand, need to care about the specifics of the business domain, not just the types. They'll contain the logic which ensures that a string is an email address, for example, complaining loudly if not. Value objects also tend to be immutable.

Understanding the use cases for each, we can ensure the consistency and integrity of exchanged data, getting the best of both worlds!

class EmployeeDTO
{
    public function __construct(
      private string $name,
      private Phone $phone,
      private Email $email,
      private Salary $salary,
      ....
    )
}

Summary

Value objects can be a really useful tool for improving the quality of your application. Adding these additional objects can initially feel like a lot of overhead for relative simple data handling, but as your application grows, the additional confidence you have in their validation will help your project grow much more quickly and sustainably. Having consistent validation throughout your application helps to make testing easier, keeping the number of bugs down, and makes for much more readable code!


CyberWiseCon 2025 Speaker

CyberWiseCon 2025

In May 2025, I'll be giving a talk at CyberWiseCon 2025 in Vilnius, Lithuania. From selling 10 Downing St, to moving the Eiffel Tower to Dublin, this talk covers real-world examples of unconventional ways to stop scrapers, phishers, and content thieves. You'll gain practical insights to protect assets, outsmart bad actors, and avoid the mistakes we made along the way!

Get your ticket now and I'll see you there!


Share This Article

Related Articles


Lazy loading background images to improve load time performance

Lazy loading of images helps to radically speed up initial page load. Rich site designs often call for background images, which can't be lazily loaded in the same way. How can we keep our designs, while optimising for a fast initial load?

Using Google Sheets as a RESTful JSON API

Save time by not building backends for simple CRUD apps. Use Google Sheets as both a free backend and JSON API endpoint!

Idempotency - what is it, and how can it help our Laravel APIs?

Idempotency is a critical concept to be aware of when building robust APIs, and is baked into the SDKs of companies like Stripe, Paypal, Shopify, and Amazon. But what exactly is idempotency? And how can we easily add support for it to our Laravel APIs?

Calculating rolling averages with Laravel Collections

Rolling averages are perfect for smoothing out time-series data, helping you to gain insight from noisy graphs and tables. This new package adds first-class support to Laravel Collections for rolling average calculation.

More