Loading learning content...
In the vast landscape of object-oriented programming, we create countless objects—data containers, service wrappers, utility classes. Most objects are defined entirely by their attributes: two Point objects with coordinates (3, 4) are interchangeable. Swap them, and nothing changes.
But some objects are fundamentally different. A Customer named "Alice" with email "alice@example.com" is not interchangeable with another Customer who happens to share the same name and email. If Alice changes her email, she's still Alice. If she moves to a new address, she's still Alice. Something about Alice persists through all these changes—her identity.
This distinction lies at the heart of Domain-Driven Design's tactical patterns. Objects whose identity matters across time and state changes are called Entities. Understanding entities deeply is essential for building systems that accurately model real-world domains where continuity of identity matters.
By the end of this page, you will understand what entities are, why they exist as a distinct concept, how they differ from other object types, and when an object in your domain should be modeled as an entity. You'll gain the foundational vocabulary and mental model needed to make correct modeling decisions in complex domains.
Let's begin with Eric Evans' definition from the foundational Domain-Driven Design text:
"Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity."
— Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
An Entity is an object that:
The key insight is that entities are not defined by what they are but by which they are. Two entities with identical attributes are still two distinct entities if they have different identities.
Think of entities as objects with "souls." Even if every visible attribute changes, the entity remains the same entity. A person can change their name, address, appearance, and occupation—but they're still the same person. That persistent "sameness" is identity, and objects with identity are entities.
Formal characteristics of an Entity:
| Characteristic | Description | Example |
|---|---|---|
| Identity | A unique identifier that distinguishes this object from all others of the same type | Customer ID, Order Number, ISBN |
| Mutability | Attributes can change while identity remains constant | Customer changes address, balance, preferences |
| Lifecycle | Has a meaningful existence through creation, modification, and potential deletion | Order is created, fulfilled, and archived |
| Referenceability | Other objects can hold references to this entity via its identity | Invoice references Customer by ID |
| Equality by Identity | Two entities are equal if and only if they have the same identity | customer1.equals(customer2) iff customer1.id == customer2.id |
You might wonder: "Can't we just use regular classes? Why do we need a special category called 'Entity'?"
The answer lies in the complexity of real-world domains and the need to model them accurately. Without the entity concept, we face several problems:
The banking domain example:
Consider a BankAccount. Without entity thinking, you might model it as:
class BankAccount {
String ownerName;
BigDecimal balance;
String accountType;
}
Now, if two accounts have the same owner name, balance, and type, are they the same account? Obviously not! In the real world, bank accounts have account numbers—unique identifiers that persist through all changes.
Without this identity:
The entity concept forces us to model identity explicitly, preventing these problems.
Entity is not just a programming pattern—it's a domain modeling concept. When domain experts talk about 'this customer' or 'that order,' they're implicitly using entity thinking. DDD makes this explicit in code, aligning technical implementation with domain understanding.
Entities appear naturally in virtually every complex domain. Recognizing them is a critical skill for domain modeling. Let's examine entities across various industries:
| Domain | Entity | Identity | Mutable Attributes |
|---|---|---|---|
| E-Commerce | Customer | CustomerId (UUID) | Name, email, address, preferences, loyalty tier |
| E-Commerce | Order | OrderNumber | Status, shipping address, line items, payment status |
| E-Commerce | Product | SKU or ProductId | Name, description, price, stock quantity, images |
| Healthcare | Patient | MedicalRecordNumber | Name, address, medical history, current medications |
| Healthcare | Appointment | AppointmentId | DateTime, status, notes, assigned doctor |
| Finance | Account | AccountNumber | Balance, status, owner, transaction history |
| Finance | Transaction | TransactionId | Status (though often immutable after creation) |
| HR System | Employee | EmployeeId | Name, department, salary, role, manager |
| HR System | Position | PositionId | Title, department, salary range, requirements |
| Logistics | Shipment | TrackingNumber | Status, location, estimated delivery, contents |
| Education | Student | StudentId | Name, enrolled courses, grades, contact info |
| Real Estate | Property | PropertyId or Address | Owner, price, status, description, features |
Pattern recognition:
Notice that in every case, the entity has:
This isn't accidental—it reflects how businesses actually work. Domain experts don't say "the customer with name 'Alice' and email 'alice@example.com'." They say "Customer ID 12345" or just "Alice's account." Identity is built into the domain language.
When domain experts naturally refer to things by identifiers ("Order #12345," "Account ending in 7890," "Case number 2024-001"), you're almost certainly looking at an entity. The domain's natural language reveals entity boundaries.
DDD distinguishes entities from other object types, particularly Value Objects. Understanding this distinction is crucial for correct modeling:
The classic illustration:
Consider money. A $20 bill in your wallet has a serial number—it's technically an entity (the government tracks it). But when you pay for something, you don't care which $20 bill you use. You care about the amount. In most domains, money is modeled as a Value Object:
// Value Object - defined by its attributes, immutable
class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {}
equals(other: Money): boolean {
return this.amount === other.amount &&
this.currency === other.currency;
}
}
But the bank account holding that money is an Entity:
// Entity - defined by identity, mutable
class BankAccount {
constructor(
public readonly accountId: AccountId, // Identity
private balance: Money // Mutable attribute
) {}
equals(other: BankAccount): boolean {
return this.accountId.equals(other.accountId);
}
credit(amount: Money): void {
this.balance = this.balance.add(amount); // State changes
}
}
The entity (BankAccount) has an identity that persists. The value object (Money) is defined by what it is, not which specific instance it is.
Whether something is an entity or value object depends on the bounded context. In a currency tracking system, each physical bill might be an entity (tracked by serial number). In a retail POS system, money is a value object (we only care about amounts). Always model based on your specific domain's needs.
Every well-designed entity has a consistent structure. Understanding this structure helps you design entities correctly:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
// A well-structured Entity in TypeScript // Strong typing for identity prevents mixing IDsclass CustomerId { constructor(public readonly value: string) { if (!value || value.trim().length === 0) { throw new Error("CustomerId cannot be empty"); } } equals(other: CustomerId): boolean { return this.value === other.value; } toString(): string { return this.value; }} // The Entity itselfclass Customer { // ======================================== // 1. IDENTITY - The soul of the entity // ======================================== private readonly id: CustomerId; // ======================================== // 2. MUTABLE STATE - Attributes that change // ======================================== private name: string; private email: Email; // Value Object private address: Address; // Value Object private loyaltyPoints: number; private tier: CustomerTier; // Enum or Value Object // ======================================== // 3. INVARIANTS - Rules that must always hold // ======================================== // - Email must be valid // - Name cannot be empty // - Loyalty points cannot be negative // - Tier must match point thresholds // ======================================== // 4. CONSTRUCTOR - Enforce valid creation // ======================================== constructor( id: CustomerId, name: string, email: Email, address: Address ) { this.id = id; this.setName(name); // Use setter for validation this.email = email; this.address = address; this.loyaltyPoints = 0; this.tier = CustomerTier.BRONZE; } // ======================================== // 5. IDENTITY ACCESSOR - Read-only access to ID // ======================================== getId(): CustomerId { return this.id; } // ======================================== // 6. BEHAVIOR METHODS - Operations that modify state // ======================================== changeEmail(newEmail: Email): void { // Could raise domain event: EmailChanged this.email = newEmail; } relocate(newAddress: Address): void { // Could raise domain event: CustomerRelocated this.address = newAddress; } earnPoints(points: number): void { if (points < 0) { throw new Error("Cannot earn negative points"); } this.loyaltyPoints += points; this.recalculateTier(); } // ======================================== // 7. PRIVATE HELPERS - Internal state management // ======================================== private setName(name: string): void { if (!name || name.trim().length === 0) { throw new Error("Customer name cannot be empty"); } this.name = name.trim(); } private recalculateTier(): void { if (this.loyaltyPoints >= 10000) { this.tier = CustomerTier.PLATINUM; } else if (this.loyaltyPoints >= 5000) { this.tier = CustomerTier.GOLD; } else if (this.loyaltyPoints >= 1000) { this.tier = CustomerTier.SILVER; } else { this.tier = CustomerTier.BRONZE; } } // ======================================== // 8. EQUALITY - Based on identity only // ======================================== equals(other: Customer): boolean { if (other === null || other === undefined) { return false; } return this.id.equals(other.id); } // ======================================== // 9. HASH CODE - Consistent with equals // ======================================== hashCode(): number { return hashString(this.id.value); }}CustomerId from being confused with OrderId.customer.relocate(address) not customer.setAddress(address).Deciding whether to model a concept as an entity is a critical design decision. Here's a systematic approach:
Ask yourself: "If I made an exact copy of this object with the same attributes, would that be a bug or perfectly fine?" If it's a bug (two customers with ID 12345 is wrong), it's an entity. If it's fine (two instances of Money(50, 'USD') are interchangeable), it's a value object.
Borderline cases:
Some concepts can be modeled either way depending on context:
| Concept | Entity Context | Value Object Context |
|---|---|---|
| Address | Property management (each address is a registered location) | E-commerce (just describes where to ship) |
| Email service (tracking delivery, read status) | User profile (just contact information) | |
| Seat | Airline reservation (this specific seat on this specific flight) | General capacity planning (we need 100 seats) |
| Color | Paint inventory (tracking specific batches) | UI styling (hex codes are interchangeable) |
The domain drives the decision. Model what matters to your specific business context.
Even experienced developers make mistakes when modeling entities. Being aware of these anti-patterns helps you avoid them:
string for CustomerId, int for age, string for email. Loses type safety and validation.customer.setId(newId) exists, the model is broken.equals() by comparing all attributes instead of identity. Breaks collections and caching.12345678910111213141516171819202122232425262728293031
// ❌ ANTI-PATTERN: Anemic Entity class Customer { public id: string; // Mutable ID! public name: string; public email: string; public balance: number; // Just a data container // No behavior, no validation // Logic scattered elsewhere} // All logic in a "service"class CustomerService { updateEmail(c: Customer, email: string) { // Validation here instead of entity if (!isValidEmail(email)) { throw new Error("Invalid email"); } c.email = email; } charge(c: Customer, amount: number) { // Business logic outside entity if (c.balance < amount) { throw new Error("Insufficient funds"); } c.balance -= amount; }}12345678910111213141516171819202122232425262728293031323334353637383940
// ✅ CORRECT: Rich Entity class Customer { private readonly id: CustomerId; private name: string; private email: Email; // Value Object private balance: Money; // Value Object constructor( id: CustomerId, name: string, email: Email ) { this.id = id; this.setName(name); this.email = email; this.balance = Money.zero(); } // Behavior lives in the entity changeEmail(newEmail: Email): void { this.email = newEmail; } charge(amount: Money): void { if (!this.balance.isGreaterThanOrEqual(amount)) { throw new InsufficientFundsError( this.id, amount ); } this.balance = this.balance.subtract(amount); } private setName(name: string): void { if (!name?.trim()) { throw new InvalidNameError(); } this.name = name.trim(); }}If your entity has only getters/setters and you have separate 'service' classes with methods like processOrder(order), validateCustomer(customer), or calculateBalance(account), you likely have anemic entities. The logic should live where the data lives.
We've established the foundational understanding of entities in Domain-Driven Design. Let's consolidate the key insights:
What's next:
Now that we understand what an entity is, we need to explore the subtle but crucial distinction between identity and equality in depth. The next page dives into how entities determine sameness, why this differs from value equality, and the technical implications for implementing equals() and hashCode() correctly.
You now understand what entities are, why they exist as a distinct concept, and when to use them. You've seen entity structure, recognized common anti-patterns, and understand how entities differ from value objects. Next, we'll explore identity versus equality in depth.