References

Entity

Concept

An Entity is a domain object defined by its identity, not its attributes. Two entities are the same if they share the same ID, regardless of whether their other attributes differ. Entities have a lifecycle — they are created, modified, and eventually deleted or archived.

Use Entity when the concept has continuous identity across time: a User is still the same user after changing their email. An Order is still the same order after adding items.


Key Characteristics

  • Unique, stable identity — an ID assigned at creation that never changes
  • Mutable state — attributes evolve over the entity’s lifecycle
  • Identity-based equalityequals() compares IDs, never attributes
  • Encapsulated behavior — business rules live inside the entity (rich model)
  • Lifecycle — created, updated, deleted/archived — tracked via events or audit fields

When to Use

Use Entity when:

  • The concept persists over time and must be tracked (User, Order, Product, Account)
  • Two instances with identical attributes should still be distinct (User("Alice") ≠ User("Alice") if different IDs)
  • The object has meaningful state changes with business rules (confirm, cancel, archive)

Don’t use Entity when:

  • The concept is defined entirely by its values with no lifecycle (use Value Object: Money, Email, Address)
  • The object is only a data container with no business behavior (use a DTO)

Core Patterns

// domain/entities/User.ts
import { UserId } from '../value-objects/UserId';
import { Email } from '../value-objects/Email';

export class User {
  constructor(
    readonly id: UserId,          // identity — assigned at creation, never mutated
    private _email: Email,
    private _name: string,
    private _status: 'active' | 'suspended' = 'active',
  ) {}

  get email(): Email  { return this._email; }
  get name(): string  { return this._name; }
  get status()        { return this._status; }

  changeEmail(newEmail: Email): void {
    if (this._status === 'suspended') {
      throw new Error('Suspended users cannot change email');
    }
    this._email = newEmail;
  }

  suspend(): void {
    if (this._status === 'suspended') throw new Error('User is already suspended');
    this._status = 'suspended';
  }

  equals(other: User): boolean {
    return this.id.equals(other.id);  // identity-based — never compare attributes
  }
}

// Usage
const user1 = new User(new UserId('1'), new Email('alice@example.com'), 'Alice');
const user2 = new User(new UserId('1'), new Email('bob@example.com'), 'Bob');

console.log(user1.equals(user2)); // true — same ID, same entity despite different attributes

Entity vs Value Object Decision

QuestionEntityValue Object
Does it have a lifecycle?✅ Yes❌ No
Does identity matter independently of attributes?✅ Yes❌ No
Can it be replaced by another instance with the same values?❌ No✅ Yes
Does it need to be tracked in a repository?✅ Yes❌ No
ExamplesUser, Order, Product, AccountMoney, Email, Address, PhoneNumber

Common Mistakes

❌ Attribute-based equality on entities:

// WRONG: comparing attributes defeats identity semantics
equals(other: User): boolean {
  return this._email.equals(other._email); // two users can share an email (e.g. family)
}

// CORRECT: always compare by ID
equals(other: User): boolean {
  return this.id.equals(other.id);
}

❌ Mutable identity:

// WRONG: ID should never change
class User {
  id: string; // public and mutable
}
user.id = 'new-id'; // breaks identity contract

// CORRECT: readonly identity
class User {
  constructor(readonly id: UserId) {}
}

❌ Business logic outside the entity (Anemic Domain Model):

// WRONG: business rule lives in a service
class UserService {
  suspend(user: User) {
    user.status = 'suspended'; // directly mutating state
  }
}

// CORRECT: business rule lives in the entity
class User {
  suspend(): void {
    if (this._status === 'suspended') throw new Error('Already suspended');
    this._status = 'suspended';
  }
}