Entity vs Value Object in Domain-Driven Design

Entity vs Value Object in Domain-Driven Design

By

8 min read··

clean-architecturecsharpddd

An entity is defined by an identity that survives changes to its attributes. A value object is defined by its values and is replaced when those values change. Use entities for concepts with a business lifecycle, such as orders, and value objects for interchangeable values, such as money or addresses.

What Is the Difference Between an Entity and a Value Object?

In Domain-Driven Design, there are two fundamental building blocks for modeling your domain:

  • Entities - defined by their identity
  • Value Objects - defined by their attributes

Two entities with the same properties are still different if they have different IDs. Two Value Objects with the same properties are equal - they're interchangeable.

This distinction drives almost every design decision in your domain model.

A side-by-side contrast: an entity has an identity, equality by Id, and is mutable via methods, while a value object has no identity, equality by attributes, and is immutable and replaced to change

Entities

An Entity is an object that has a unique identity that persists over time. Even if every attribute changes, the entity is still the same object.

public sealed class Customer : Entity
{
    private Customer(Guid id, string name) : base(id)
    {
        Name = name;
    }

    public string Name { get; private set; }

    public static Customer Create(string name)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        return new Customer(Guid.NewGuid(), name.Trim());
    }

    public void ChangeName(string newName)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(newName);
        Name = newName.Trim();
    }
}

Key characteristics:

  • Has an ID - Customer with ID abc-123 is always that customer, even if they change their name
  • Mutable (controlled) - can change over time through domain methods
  • Equality by identity - two customers are equal only if they have the same ID
  • Has a lifecycle - created, modified, possibly deleted

Entity Base Class

public abstract class Entity : IEquatable<Entity>
{
    protected Entity() { } // Used when EF Core materializes derived entities.

    protected Entity(Guid id)
    {
        if (id == Guid.Empty)
            throw new ArgumentException("An entity needs an ID.", nameof(id));

        Id = id;
    }

    public Guid Id { get; private set; }

    public bool Equals(Entity? other) =>
        ReferenceEquals(this, other) ||
        (other is not null && GetType() == other.GetType() &&
         Id != Guid.Empty && Id == other.Id);

    public override bool Equals(object? obj) =>
        obj is Entity entity && Equals(entity);

    public override int GetHashCode() => HashCode.Combine(GetType(), Id);
}

The type check prevents an Order and a Customer with the same GUID from comparing equal. This example uses sealed entities and application-generated IDs; proxy types and database-generated identities need a different equality policy.

Value Objects

A Value Object is an immutable object that represents a concept with no identity. It's defined entirely by its attributes.

public sealed record Money
{
    private Money() { } // Allows materialization without a public setter.

    private Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public decimal Amount { get; private init; }
    public string Currency { get; private init; } = null!;

    public static Money Zero(string currency) => Create(0, currency);

    public static Money Create(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative.");

        ArgumentException.ThrowIfNullOrWhiteSpace(currency);
        string normalized = currency.Trim().ToUpperInvariant();
        if (normalized.Length != 3 || normalized.Any(c => c < 'A' || c > 'Z'))
            throw new ArgumentException("Use a three-letter currency code.", nameof(currency));

        return new Money(amount, normalized);
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Cannot add different currencies.");

        return new Money(Amount + other.Amount, Currency);
    }

}

The record supplies equality for Amount and Currency. This model allows nonnegative prices and validates the shape of a currency code; a payment system should also check its supported currencies and rounding rules.

Key characteristics:

  • No identity - Money(100, "USD") is equal to any other Money(100, "USD")
  • Immutable - once created, it never changes. Operations return new instances
  • Equality by attributes - two Money objects are equal if amount and currency match
  • Validated construction - the factory enforces the rules this model defines
  • Contains behavior - Add checks currencies before producing a new value

Side-by-Side Comparison

AspectEntityValue Object
IdentityBusiness identity persists over timeDefined by its values
EqualitySame entity type and identitySame constituent values
ChangesControlled changes preserve identityReplace with a new value
ExamplesCustomer, Order, ProductMoney, Email, DateRange
EF Core mappingEntity type with a key; tables may be sharedComplex type, owned type, or conversion
ConstructionConstructor or factory with validationConstructor or factory with validation

When to Use Each

Use an Entity when:

  • The object has a unique identity that matters to the business
  • You need to distinguish between two objects with identical attributes
  • The object changes over time and you need to track those changes

Examples:

  • Customer - two customers named "John" are different people
  • Order - each order is unique, even with identical items
  • Product - products have SKUs and need individual tracking

Use a Value Object when:

  • The object is defined by what it is, not who it is
  • Two objects with the same properties are interchangeable
  • The object should be immutable
  • The concept carries validation rules or behavior

Examples:

  • Email - "[email protected]" is the same regardless of who holds it
  • Money - $100 USD is $100 USD, no matter which account it's in
  • Address - same street, city, zip = same address
  • DateRange - same start and end = same range

A Practical Example

Consider an e-commerce domain:

public readonly record struct CustomerId(Guid Value);
public readonly record struct ProductId(Guid Value);
public sealed record Address(string Street, string City);
public enum OrderStatus { Draft, Submitted }

public sealed class Order : Entity
{
    private readonly List<OrderLineItem> _lineItems = new();

    private Order() { }

    private Order(Guid id, CustomerId customerId, Address shippingAddress)
        : base(id)
    {
        CustomerId = customerId;
        ShippingAddress = shippingAddress;
        Status = OrderStatus.Draft;
        TotalAmount = Money.Zero("USD");
    }

    public CustomerId CustomerId { get; private set; }
    public Address ShippingAddress { get; private set; } = null!;
    public Money TotalAmount { get; private set; } = null!;
    public OrderStatus Status { get; private set; }
    public IReadOnlyCollection<OrderLineItem> LineItems => _lineItems.AsReadOnly();

    public static Order Create(CustomerId customerId, Address shippingAddress)
    {
        if (customerId.Value == Guid.Empty)
            throw new ArgumentException("A customer ID is required.", nameof(customerId));

        ArgumentNullException.ThrowIfNull(shippingAddress);
        return new Order(Guid.NewGuid(), customerId, shippingAddress);
    }

    public void AddLineItem(ProductId productId, Money unitPrice, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Only draft orders can change.");

        ArgumentNullException.ThrowIfNull(unitPrice);
        if (unitPrice.Currency != TotalAmount.Currency)
            throw new ArgumentException("The order uses USD.", nameof(unitPrice));

        var lineItem = new OrderLineItem(productId, unitPrice, quantity);
        var newTotal = TotalAmount.Add(lineItem.TotalPrice);
        _lineItems.Add(lineItem);
        TotalAmount = newTotal;
    }

    public void UpdateShippingAddress(Address newAddress)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Only draft orders can change.");

        ArgumentNullException.ThrowIfNull(newAddress);
        ShippingAddress = newAddress; // Replace, don't mutate
    }
}

public sealed class OrderLineItem : Entity
{
    private OrderLineItem() { }

    internal OrderLineItem(ProductId productId, Money unitPrice, int quantity)
        : base(Guid.NewGuid())
    {
        if (productId.Value == Guid.Empty)
            throw new ArgumentException("A product ID is required.", nameof(productId));
        if (quantity <= 0)
            throw new ArgumentOutOfRangeException(nameof(quantity));

        ProductId = productId;
        UnitPrice = unitPrice;
        Quantity = quantity;
    }

    public ProductId ProductId { get; private set; }
    public Money UnitPrice { get; private set; } = null!;
    public int Quantity { get; private set; }
    public Money TotalPrice => Money.Create(UnitPrice.Amount * Quantity, UnitPrice.Currency);
}

Notice the pattern:

  • Order and OrderLineItem are Entities - they have identity and change over time
  • Money, Address, CustomerId, ProductId are Value Objects - they represent concepts, compare by their values, and are immutable

Common Mistakes

1. Making everything an Entity. Use a value object when the business treats equal values as interchangeable. The same concept can be an entity in another bounded context, such as an address managed by a property registry.

2. Making Value Objects mutable. Value Objects must be immutable. If you need to change a value, create a new instance:

// Wrong - mutating a value object
address.City = "New York";

// Right - replacing with a new instance
var newAddress = new Address("123 Main St", "New York");
order.UpdateShippingAddress(newAddress);

3. Using primitive types where Value Objects belong. If a string carries business rules (email format, phone number format), wrap it in a Value Object. See Strongly Typed IDs for another example.

4. Overcomplicating equality. Entities compare by ID. Value Objects compare by attributes. Don't mix these up.

Persisting Each Type With EF Core

In the infrastructure project, put these mappings inside DbContext.OnModelCreating. They use the types above and EF Core 8 or later with a relational provider:

modelBuilder.Entity<Order>(entity =>
{
    entity.HasKey(o => o.Id);
    entity.Property(o => o.Id).ValueGeneratedNever();
    entity.Property(o => o.CustomerId)
        .HasConversion(id => id.Value, value => new CustomerId(value));
    entity.HasMany(o => o.LineItems).WithOne().HasForeignKey("OrderId");
    entity.Navigation(o => o.LineItems).UsePropertyAccessMode(PropertyAccessMode.Field);
});

modelBuilder.Entity<OrderLineItem>(entity =>
{
    entity.HasKey(li => li.Id);
    entity.Property(li => li.Id).ValueGeneratedNever();
    entity.Property(li => li.ProductId)
        .HasConversion(id => id.Value, value => new ProductId(value));
    entity.Ignore(li => li.TotalPrice);
    entity.ComplexProperty(li => li.UnitPrice, money =>
    {
        money.Property(m => m.Amount).HasPrecision(18, 2);
        money.Property(m => m.Currency).HasMaxLength(3);
    });
});

Value Objects use Complex Types (EF Core 8+) or Owned Types:

modelBuilder.Entity<Order>(entity =>
{
    entity.ComplexProperty(o => o.ShippingAddress, addressBuilder =>
    {
        addressBuilder.Property(a => a.Street).HasColumnName("shipping_street");
        addressBuilder.Property(a => a.City).HasColumnName("shipping_city");
    });

    entity.ComplexProperty(o => o.TotalAmount, moneyBuilder =>
    {
        moneyBuilder.Property(m => m.Amount).HasColumnName("total_amount").HasPrecision(18, 2);
        moneyBuilder.Property(m => m.Currency).HasColumnName("total_currency").HasMaxLength(3);
    });
});

I cover the full set of mapping options in How to Model Value Objects With EF Core.

Summary

The Entity vs Value Object distinction is one of the most important modeling decisions in DDD:

  • Entities preserve identity. Different instances can represent the same entity when their type and ID match.
  • Value objects compare by values. Equal instances are interchangeable in the domain model.

Choose a value object when values are interchangeable, and an entity when the business needs to track an identity over time.

Thanks for reading.

And stay awesome!


Frequently Asked Questions

What is the difference between an entity and a value object in DDD?

An entity is defined by its identity: two entities with the same attributes but different IDs are different objects. A value object is defined by its attributes: two value objects with the same values are equal and interchangeable.

Should a value object be immutable?

Yes. Value objects never change after creation. Any operation that would modify one returns a new instance instead, which makes them safe to share and easy to reason about.

Is Money an entity or a value object?

A value object. One hundred dollars is one hundred dollars regardless of which account holds it. It has no identity, only an amount and a currency.

How do you persist value objects with EF Core?

Use complex types (EF Core 8 and later), owned types, or value conversions for single-property wrappers. Entities have primary keys, but their table layout can use separate tables, table sharing, or inheritance mapping.

Should I default to entities or value objects when modeling?

Prefer a value object when equal values are interchangeable in the business model. Use an entity when the business needs to track one identity through changes. The choice depends on the bounded context.

  • Aggregate Design in DDD - Rules, Boundaries, and Consistency

    Aggregates are the most important tactical pattern in Domain-Driven Design. They define consistency boundaries, enforce invariants, and protect your domain model. Here are the rules and practical guidance for designing aggregates in .NET.

  • Aggregate Root in DDD: Rules and Implementation

    The Aggregate Root is the gatekeeper of consistency in Domain-Driven Design. Here are the rules for designing aggregate roots and implementing them in C#.

  • The Always-Valid Domain Model

    An always-valid domain model enforces business invariants at construction and on every state change. Private constructors, validated value objects, and guarded methods reduce repeated validation while keeping persistence and concurrency checks explicit.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.