Skip to main content

Overview

Value Objects are immutable objects that represent descriptive aspects of the domain with no conceptual identity. Unlike entities, they are defined by their attributes rather than a unique identifier.

What are Value Objects?

In Domain-Driven Design, a Value Object is:
  • Immutable - Once created, it cannot be changed
  • Identity-less - Equality is based on attribute values, not an ID
  • Self-validating - Ensures it’s always in a valid state
  • Replaceable - To “change” a value object, create a new one

When to Use Value Objects

Use Value Objects when:
  • The concept has no identity (e.g., Money, Address, DateRange)
  • It measures, quantifies, or describes something in the domain
  • It can be immutable
  • Equality should be based on value, not identity
  • It groups related attributes that should always be consistent together

Value Object Pattern

While the architecture doesn’t include a base ValueObject class, you can implement value objects following these patterns:

Basic Value Object Implementation

namespace Domain.ValueObjects
{
    public class Money : IEquatable<Money>
    {
        public decimal Amount { get; }
        public string Currency { get; }

        // Private constructor to enforce factory method
        private Money(decimal amount, string currency)
        {
            Amount = amount;
            Currency = currency;
        }

        // Factory method with validation
        public static Money Create(decimal amount, string currency)
        {
            if (amount < 0)
                throw new ArgumentException("Amount cannot be negative", nameof(amount));
            
            if (string.IsNullOrWhiteSpace(currency))
                throw new ArgumentException("Currency is required", nameof(currency));
            
            if (currency.Length != 3)
                throw new ArgumentException("Currency must be 3-letter ISO code", nameof(currency));

            return new Money(amount, currency.ToUpperInvariant());
        }

        // Value-based equality
        public bool Equals(Money other)
        {
            if (other is null) return false;
            return Amount == other.Amount && Currency == other.Currency;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as Money);
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(Amount, Currency);
        }

        public static bool operator ==(Money left, Money right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Money left, Money right)
        {
            return !Equals(left, right);
        }

        public override string ToString()
        {
            return $"{Amount:N2} {Currency}";
        }
    }
}

Advanced Value Object with Operations

namespace Domain.ValueObjects
{
    public class EmailAddress : IEquatable<EmailAddress>
    {
        private static readonly Regex EmailRegex = new Regex(
            @"^[^@\s]+@[^@\s]+\.[^@\s]+$",
            RegexOptions.Compiled | RegexOptions.IgnoreCase);

        public string Value { get; }

        private EmailAddress(string value)
        {
            Value = value;
        }

        public static EmailAddress Create(string email)
        {
            if (string.IsNullOrWhiteSpace(email))
                throw new ArgumentException("Email address is required", nameof(email));

            email = email.Trim().ToLowerInvariant();

            if (!EmailRegex.IsMatch(email))
                throw new ArgumentException("Invalid email format", nameof(email));

            return new EmailAddress(email);
        }

        public string GetDomain()
        {
            return Value.Split('@')[1];
        }

        public bool Equals(EmailAddress other)
        {
            if (other is null) return false;
            return Value == other.Value;
        }

        public override bool Equals(object obj) => Equals(obj as EmailAddress);
        public override int GetHashCode() => Value.GetHashCode();
        public static bool operator ==(EmailAddress left, EmailAddress right) => Equals(left, right);
        public static bool operator !=(EmailAddress left, EmailAddress right) => !Equals(left, right);
        public override string ToString() => Value;
    }
}

Common Value Object Examples

Address Value Object

public class Address : IEquatable<Address>
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string PostalCode { get; }
    public string Country { get; }

    private Address(string street, string city, string state, string postalCode, string country)
    {
        Street = street;
        City = city;
        State = state;
        PostalCode = postalCode;
        Country = country;
    }

    public static Address Create(string street, string city, string state, string postalCode, string country)
    {
        if (string.IsNullOrWhiteSpace(street))
            throw new ArgumentException("Street is required", nameof(street));
        if (string.IsNullOrWhiteSpace(city))
            throw new ArgumentException("City is required", nameof(city));
        if (string.IsNullOrWhiteSpace(country))
            throw new ArgumentException("Country is required", nameof(country));

        return new Address(street, city, state, postalCode, country);
    }

    public bool Equals(Address other)
    {
        if (other is null) return false;
        return Street == other.Street &&
               City == other.City &&
               State == other.State &&
               PostalCode == other.PostalCode &&
               Country == other.Country;
    }

    public override bool Equals(object obj) => Equals(obj as Address);
    
    public override int GetHashCode()
    {
        return HashCode.Combine(Street, City, State, PostalCode, Country);
    }

    public override string ToString()
    {
        return $"{Street}, {City}, {State} {PostalCode}, {Country}";
    }
}

DateRange Value Object

public class DateRange : IEquatable<DateRange>
{
    public DateTime Start { get; }
    public DateTime End { get; }

    private DateRange(DateTime start, DateTime end)
    {
        Start = start;
        End = end;
    }

    public static DateRange Create(DateTime start, DateTime end)
    {
        if (start > end)
            throw new ArgumentException("Start date must be before end date");

        return new DateRange(start, end);
    }

    public int DurationInDays()
    {
        return (End - Start).Days;
    }

    public bool Contains(DateTime date)
    {
        return date >= Start && date <= End;
    }

    public bool Overlaps(DateRange other)
    {
        return Start <= other.End && other.Start <= End;
    }

    public bool Equals(DateRange other)
    {
        if (other is null) return false;
        return Start == other.Start && End == other.End;
    }

    public override bool Equals(object obj) => Equals(obj as DateRange);
    public override int GetHashCode() => HashCode.Combine(Start, End);
    public override string ToString() => $"{Start:yyyy-MM-dd} to {End:yyyy-MM-dd}";
}

Enums and Constants

The architecture includes examples of domain enums in Domain/Enums/Enums.cs:
public enum DummyValues
{
    value1,
    value2,
    value3,
}

public enum DatabaseType
{
    MYSQL,
    MARIADB,
    SQLSERVER, 
    MONGODB
}
Domain constants are defined in Domain/Constants/Constants.cs:6:
internal static class DomainConstants
{
    public const string NOTNULL_OR_EMPTY = "El valor no puede ser nulo o vacio.";
}

Utility Classes

The architecture provides utility classes in Domain/Others/Utils/:

EnumUtils

Helper methods for working with enums (Domain/Others/Utils/EnumUtils.cs:5):
public static class EnumUtils
{
    public static T ToEnum<T>(this string value) where T : struct, Enum
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException(DomainConstants.NOTNULL_OR_EMPTY, nameof(value));
        }
        if (Enum.TryParse<T>(value, true, out var result))
        {
            return result;
        }
        throw new ArgumentException($"El valor '{value}' no es un miembro válido del enum {typeof(T).Name}.");
    }
}
Usage example:
var dbType = "MYSQL".ToEnum<DatabaseType>();

Best Practices

Immutability

Use readonly properties and private constructors. Create new instances instead of modifying existing ones.

Validation

Validate in the factory method to ensure invalid value objects cannot exist.

Equality

Implement IEquatable<T> and override equality operators based on value, not reference.

Factory Methods

Use static Create() methods instead of public constructors for controlled instantiation.

Value Objects vs Entities

AspectValue ObjectEntity
IdentityNo unique identifierHas unique ID
EqualityBased on valuesBased on ID
MutabilityImmutableMutable through methods
LifespanShort-livedLong-lived
ExampleMoney, AddressCustomer, Order

Integration with Entities

Use value objects as properties in your entities:
public class Customer : DomainEntity<Guid, CustomerValidator>
{
    public string Name { get; private set; }
    public EmailAddress Email { get; private set; }  // Value Object
    public Address ShippingAddress { get; private set; }  // Value Object
    public Money AccountBalance { get; private set; }  // Value Object

    public void UpdateEmail(EmailAddress newEmail)
    {
        Email = newEmail ?? throw new ArgumentNullException(nameof(newEmail));
    }

    public void ChangeAddress(Address newAddress)
    {
        ShippingAddress = newAddress ?? throw new ArgumentNullException(nameof(newAddress));
    }
}

Build docs developers (and LLMs) love