A Value Object is an object that gets its equality from the values of it’s properties instead of an identity and is one of the key building blocks to DDD software. Some examples would be money, a date and a person’s name. When you learned programming it’s highly probable you first learned to pass your software’s data between objects using built-in types (e.g GUID, string, int, bool, decimal, etc) declared by your language of choice. While this is OK to learn on it’s arguably too loose and prone to errors for commercial grade software. When passing data between objects and representing data within entities it’s advisable to use Value Objects because they guarantee data validity.

Already familiar with the basics? Read Value Objects vs Primitives – When Not to Use Primitives?

Value Objects are deeply immutable and represent an integral unit of data. You can use them to describe domain entities, as method return values and as method parameters.

Throughout this article I’m going to use a value object that’s practical in real world software because of its common requirements across software products. This value object represents a person’s legal name.

namespace Company.Domain.User;

public class UserLegalName : ValueObjectBase<UserLegalName>
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }

    // ORMs (e.g Entity Framework) need an empty constructor
    private UserLegalName() { }

    public UserLegalName(string firstName, string lastName)
    {
        SetFirst(firstName);
        SetLast(lastName);
    }

    private void SetLast(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new LastNameException("Last name must not be empty.");
        }

        LastName = name;
    }

    private void SetFirst(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new FirstNameException("First name must not be empty.");
        }

        FirstName = name;
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return FirstName;
        yield return LastName;
    }
}

The definition of ValueObjectBase<T> is shown at the end of this article.

UserLegalName can then be implemented by a User aggregate root as shown below.

namespace Company.Domain.User;

public class User : IAggregateRoot
{
    public Guid Id { get; private set; }
    public UserLegalName Name { get; private set; }

    public User(UserLegalName name)
    {
        Id = Guid.NewGuid();
        Name = name;
    }

    public void ChangeName(UserLegalName name)
    {
        Name = name;
        DomainEvents.Raise(new UserLegalNameChangedEvent(Id));
    }
}

Finally let’s see how we interact with the model to create a user and change an existing user’s name.

namespace Company.Api.Controllers;

public class UserController : BaseController
{
    private IUserRepository _userRepository;

    public UserController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    [HttpPost]
    public void CreateUser(string firstName, string lastName)
    {
        var user = new User(new UserLegalName(firstName, lastName));
        _userRepository.Add(user);
        _userRepository.Save();
    }

    [HttpPatch]
    public void ChangeName(Guid userId, string firstName, string lastName)
    {
        var user = _userRepository.UserOfId(userId);
        user.ChangeName(new UserLegalName(firstName, lastName))
        _userRepository.Save();
    }
}

For brevity I left out basic validation control flow that should be in the API controller, but nevertheless the domain model itself is quite clean and concise. Even though the presence of the Value Objects requires you to instantiate one additional object (the UserLegalName) in my opinion the benefits outweigh any downsides.

The Rules of Value Objects

There are four rules an object must follow to classify as a Value Object.

  1. Must be immutable, that is, after instantiation no further changes can be made.
  2. Should never have an ID (e.g. a GUID or int) used by the persistence layer for lookup.
  3. Equality must be based on the values of their properties.
  4. Must fail to instantiate if any one of their properties are invalid.

Immutability

After instantiation it’s internal state can never be modified. When you need to change the underlying state you have to create a new instance. The example below illustrates this.

namespace Company.Api.Controllers;

public class UserController : BaseController
{
    private IUserRepository _userRepository;

    public UserController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    [HttpPatch]
    public void ChangeName(Guid userId, string firstName, string lastName)
    {
        var user = _userRepository.UserOfId(userId);
        user.ChangeName(new UserLegalName(firstName, lastName))
        _userRepository.Save();
    }
}

A few notes about the example above. It’s completely preferential whether to create a ChangeName(UserLegalName) method on the User entity vs declaring the User entity’s Name property with a public setter. I’ll write about this pattern in more detail in a different article.

No Identity

They never have an identity used to look up and distinguish each one from the others in the persistence layer. An object with an ID that never changes is an entity. We don’t care to identify a value object as being the same “thing” as its values change over time.

For example if a User (an entity) changes their email (a value object) they are still the same person they were before. However when a user changes their email the email is a new “thing” therefore we outright replace the email instance that used to be associated to the user with the new email instance.

Equality by Property Values

Value objects are equal when their properties have the same values. Even when they reference different instances in memory. This is because they don’t have an ID that distinguishes their identities like an entity.

[Fact]
public void ValueObjectEquality()
{
    var jasonHarris = new UserLegalName("Jason", "Harris");
    var jasonHarrisDoppleganger = new UserLegalName("Jason", "Harris");
    var jasonMalone = new UserLegalName("Jason", "Malone");

    Assert.Equal(jasonHarris, jasonHarrisDoppleganger); // Assertion is true
    Assert.NotEqual(jasonHarris, jasonMalone); // Assertion is true
}

Validity

One of their best features is to validate the data during instantiation otherwise they fail. This guarantees their state is always valid when you’re referencing one (as long as you’ve designed their validation logic correctly). This also means the code that instantiates your value object can know when the inputs are invalid.

[Fact]
public void ValueObjectValidity()
{
    // First and last name must always be specified and valid
    Assert.Throws<FirstNameException>(() => new UserLegalName(string.Empty, "Harris"));
    Assert.Throws<FirstNameException>(() => new UserLegalName("  ", "Harris"));
    Assert.Throws<LastNameException>(() => new UserLegalName("Jason", string.Empty));
    Assert.Throws<LastNameException>(() => new UserLegalName("Jason", "  "));
    Assert.NotNull(new UserLegalName("Jason", "H"));
}

Unnecessary Complexity or Simplicity?

It’s surprising how many software developers are against this concept at first. Value Objects receive flack by some developers because they think they add unnecessary complexity to the software. The reality is, this is simply not the case. Eventually some of these developers realize the true power of Value Objects and whole heartedly accept them. More times than not Value Objects simplify the software, usually by reducing duplicate validation logic.

In fact, if you haven’t noticed already, most built in types are value objects by design! Assigning a variable of type string, GUID or bool (to name a few) a new value doesn’t modify the previous object instance it creates a new instance. The example below illustrates this with strings.

[Fact]
public void StringsAreValueTypes()
{
    var str1 = "Jason";
    var str2 = str1;
    str1 = "Harris";

    Debug.WriteLine(str1); // Output "Harris"
    Debug.WriteLine(str2); // Output "Jason"
}

Why Wrap Built-In Value Types with Your Own?

Alright, so now you’re probably wondering why you should concern yourself with wrapping built-in immutable value types within your own value types. Creating your own value types makes your software clearer and allows you to enforce validity.

You can continue passing around one or two string parameters to represent first name and last name if that’s what you desire. However, without your own value type to represent a user’s full legal name you’ll never be certain the strings are valid name values. Unless you duplicate your name validation logic everywhere you accept those properties as inputs. Overtime this code duplication quickly builds up and causes your software quality to dwindle into nothingness.

Value Object Base Type

Sometimes it’s helpful to assign every value object in your software a marker interface like IValueObject. ValueObjectBase<T> defines basic equality logic that would otherwise be duplicated across multiple concrete value object types.

namespace Company.Domain.Common;

/// <summary>
/// A marker interface for value objects.
/// </summary>
public interface IValueObject
{

}
namespace Company.Domain.Common;

/// <summary>
/// Abstract base type for Value Objects.
/// </summary>
/// <typeparam name="T">The value object type to assign</typeparam>
public abstract class ValueObjectBase<T> : IValueObject where T : class
{
    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObjectBase<T>)obj;
        var thisValues = GetAtomicValues().GetEnumerator();
        var otherValues = other.GetAtomicValues().GetEnumerator();
        try
        {
            while (thisValues.MoveNext() && otherValues.MoveNext())
            {
                if (ReferenceEquals(thisValues.Current, null) ^
                    ReferenceEquals(otherValues.Current, null))
                {
                    return false;
                }

                if (thisValues.Current != null &&
                    !thisValues.Current.Equals(otherValues.Current))
                {
                    return false;
                }
            }
        }
        finally
        {

            thisValues.Dispose();
            otherValues.Dispose();
        }

        return !thisValues.MoveNext() && !otherValues.MoveNext();
    }

    public override int GetHashCode()
    {
        return GetAtomicValues()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }

    public static bool operator ==(ValueObjectBase<T> left, ValueObjectBase<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ValueObjectBase<T> left, ValueObjectBase<T> right)
    {
        return !Equals(left, right);
    }

    protected abstract IEnumerable<object> GetAtomicValues();
}