Entities and Value Objects in Domain-Driven Design

The Building Blocks of a Domain Model

Software Design

A domain concept modelled as a primitive carries no rules. The code that works with it must decide what is valid and what the constraints are. In a small codebase, that is manageable. As the domain grows, the same decisions get made in different places, differently.

Domain-driven design distinguishes between two kinds of objects: value objects and entities.1 The distinction is not about complexity or size. It is about whether identity matters. Together they are the first tactical building blocks of a domain model.

The previous articles established the strategic picture and matched an architecture to each bounded context. Inside the core subdomain, the domain model sits at the center, isolated from infrastructure. This article starts filling in what that model contains: the objects that carry domain rules and the vocabulary the model is built from.

The Problem With Primitive Domain Concepts

Using primitives to represent domain concepts is a pattern known as Primitive Obsession. A course title is a string. A price is a decimal. An email is a string. The concepts have rules: a title cannot be empty, a price must have a currency, an email must match a format. But those rules have nowhere to live when the concept is just a primitive. The vocabulary disappears with them too. A String says nothing about what it represents in the domain.

On the learning platform, a title arrives from a form and the controller validates it. But the same title can be set programmatically in a test, created by a background job, or imported from an external source, none of which go through the controller. Each new entry point is another place where the validation might be forgotten. When the rule changes, every one of those places needs to be updated. Some will be missed.

No Identity to Preserve

A value object has no identity. It is defined entirely by its attributes. Two Price instances representing 49.99 EUR are the same value regardless of where or when they were created. There is no meaningful “same price over time.” You do not track a price through change; you replace it.

On the learning platform, CourseTitle, Price, and Email are value objects. A course title is a string with constraints. A price is an amount and a currency together. Separating them produces a number that means nothing without its currency. An email is a validated format, not a record to be tracked.

CourseTitle, Price, and Email as value objects defined entirely by their attributes
CourseTitle, Price, and Email as value objects defined entirely by their attributes

Value objects do not change. When the price of a course is updated, the old Price is discarded and a new one takes its place. Consider a Price passed to a discount calculator. If Price were mutable, the calculator could reduce the amount in place and the course would show the discounted price everywhere, not just at checkout. Instead, the calculator returns a new discounted Price, leaving the original unchanged. Replacing rather than mutating eliminates that class of bug.

Equality is by all attributes: two Price objects with the same amount and currency are equal, two with different currencies are not, even if the amounts match.

Valid by Construction

The practical payoff is that the rules travel with the concept. CourseTitle enforces at construction that a title is not empty and does not exceed a character limit. Price prevents combining amounts in different currencies. Email rejects strings that are not valid email addresses. These constraints are not checked in the controller or the service; they are enforced by the type itself.

If a CourseTitle exists, it is valid. An invalid one cannot be created. This is the same entry point problem from before, solved at the boundary: regardless of where a CourseTitle is constructed, the rule is enforced.

The developer experience shifts noticeably. When a title validation rule changes, there is one place to change it: the CourseTitle type. When you write a test that involves a price, you cannot accidentally omit the currency because the type requires it. Code that holds a CourseTitle already knows it is valid. The concept and its rules are in the same place.

There is a second payoff beyond the rules. The types themselves become the vocabulary of the domain. CourseTitle, Price, and Email are words a domain expert would recognise. A codebase that uses them reads like the domain it models. This is what the ubiquitous language from the earlier articles looks like in practice: the same words in conversation, in design, and in code.

Identity Persists Through Change

Not every concept can be replaced when it changes. An entity is an object defined by its identity, not its attributes. A course can have its title changed, its price updated, its list of lessons reorganized. It is still the same course. The course identifier is what makes it the same course over time. Changing an attribute is not creating a new course; it is updating an existing one.

On the learning platform, Course is an entity. So is Lesson. A lesson belongs to a specific place in a curriculum. Editing its content, reordering it, or retitling it does not create a new lesson. The lesson’s identity persists through those changes, and the system tracks which lesson a student completed, which version they watched, and whether they passed the associated exercise.

A Course entity with the same identifier before and after its title and price are updated
A Course entity with the same identifier before and after its title and price are updated

Two entities with the same attributes are not the same entity. Two courses with identical titles, prices, and lesson lists are still two different courses if they have different identifiers. Equality for entities is by identity, not by value.

The identifier is itself a value object. Typing it as CourseId rather than a raw string means a lesson identifier cannot be accidentally passed where a course identifier is expected.

The practical consequence shows up when something needs to change. To update a course title, you find the course by its identifier and set the new title. You are not creating a new course; the same course now has a different title. This is what it means for identity to persist through change.

The Context Determines the Classification

Deciding between an entity and a value object comes down to one question: does identity matter? Does it make sense to ask whether this is the same object as before, even after its attributes have changed? Then it is an entity. Is it defined entirely by its value, such that two instances with the same attributes are interchangeable? Then it is a value object.

On the learning platform, Course and Lesson are entities: both have identifiers and are tracked through change. So is Enrollment. An enrollment has a student, a course, a start date, and a status that changes over time. The same enrollment is tracked through its lifecycle.

CourseTitle is a value object. Two titles with the same text are the same title. There is no lifecycle for a title, no tracking over time. When the title changes, the old value is replaced. Price is a value object. A price is an amount and a currency. Replacing the price of a course is creating a new value, not updating a record.

The classification is not a property of the concept itself. It depends on what the context needs to do with it. A Price is a value object in the billing context: it is an amount and a currency, and there is no need to track it over time. If the platform introduced a pricing history feature that recorded when prices changed and what they were, the same concept would need an identifier and a lifecycle. The boundary is drawn by the context, not by the concept.

When the distinction is unclear, defaulting to a value object is the safer choice. Value objects are simpler to reason about, easier to test, and carry fewer assumptions about how the system will use them.

In practice, entities and value objects work together. A Course entity has a CourseTitle and a Price. The entity provides identity and tracks the course through its lifecycle. The value objects carry the rules for what a valid title and price look like. The entity does not need to know how to validate a title or enforce a currency constraint. That belongs to the value object. The building blocks compose.

What Comes Next

Entities and value objects give the model its building blocks, each carrying its own rules and its own place in the design. The value object enforces its constraints at construction, the entity controls what can change about itself.

But the rules do not stop at the boundary of a single object. A course cannot be published without at least one lesson. That rule involves the course and its lessons together. Who is responsible for enforcing it? The next article covers aggregates: the consistency boundaries that determine which objects belong together and who controls access to the whole group.


  1. Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. ↩︎

This article is part of the Domain-Driven Design series

  1. 1. What Is Domain-Driven Design?
  2. 2. Subdomains and Bounded Contexts in Domain-Driven Design
  3. 3. Context Mapping in Domain-Driven Design
  4. 4. Integrating Bounded Contexts
  5. 5. Matching Architecture to the Subdomain
  6. 6. Entities and Value Objects in Domain-Driven Design
domain-driven-design