entities

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DDD Entities & Aggregates

DDD实体与聚合根

1. Core Principles (RFC 2119)

1. 核心原则(RFC 2119)

Entities represent domain concepts with a distinct identity (usually a
UUID
). Their primary role is to encapsulate state and business logic, ensuring that the object is always valid. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
  • Identity: Every entity MUST have an explicit identifier (e.g.,
    folderId
    ,
    userId
    ) typed as a Value Object (e.g.,
    UUID
    ).
  • Encapsulation: State MUST be
    private
    for mutable properties, and
    private readonly
    for identifiers and immutable creation dates. Public fields are STRICTLY FORBIDDEN.
  • No Setters: You MUST NOT use
    set
    accessors or generic
    update(data)
    methods. You MUST create domain-specific behavior methods indicating intent (e.g.,
    rename(newName: FolderName)
    instead of
    setName(newName: string)
    ).
  • Constructors: Constructors MUST always take a single
    params
    object pattern (
    constructor(params: { ... })
    ). They MUST ONLY accept valid domain types (Value Objects or native Dates), never primitives.
  • Factories / Hydration: The constructor is primarily used by the
    infrastructure/
    layer to reconstruct (hydrate) entities from the database, or by use cases when creating completely new entities.
  • Getters: Expose internal state only when necessary using explicit getters (e.g.,
    getId()
    ,
    getName()
    ). These getters MUST return Value Objects or Dates, never primitives.
  • Snapshots: Every entity MUST define a
    <EntityName>Snapshot
    type and expose a
    toSnapshot()
    method. This method MUST return a plain object containing only Branded Primitives (via
    .toBranded()
    ) and native Dates. The snapshot MUST NEVER include truly private or internal data (such as password hashes or internal security states).
实体代表具有唯一标识(通常为
UUID
)的领域概念。它们的核心作用是封装状态与业务逻辑,确保对象始终处于有效状态。 本文档中的关键词"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"MAY"和"OPTIONAL"的含义需按照RFC 2119中的定义进行解读。
  • 标识:每个实体必须具备一个显式标识符(例如
    folderId
    userId
    ),且该标识符的类型为值对象(例如
    UUID
    )。
  • 封装:可变属性的状态必须设为
    private
    ,标识符和不可变的创建时间需设为
    private readonly
    。严格禁止使用公共字段。
  • 禁止Setter:不得使用
    set
    访问器或通用的
    update(data)
    方法。必须创建体现业务意图的领域特定行为方法(例如使用
    rename(newName: FolderName)
    而非
    setName(newName: string)
    )。
  • 构造函数:构造函数必须始终采用单个
    params
    对象模式(
    constructor(params: { ... })
    )。只能接受有效的领域类型(值对象或原生Date类型),不得接受原始类型。
  • 工厂/数据还原:构造函数主要由
    infrastructure/
    层用于从数据库中重构(还原)实体,或用例用于创建全新实体。
  • 访问器(Getters):仅在必要时通过显式访问器暴露内部状态(例如
    getId()
    getName()
    )。这些访问器必须返回值对象或Date类型,不得返回原始类型。
  • 快照(Snapshots):每个实体必须定义一个
    <EntityName>Snapshot
    类型,并暴露
    toSnapshot()
    方法。该方法必须返回一个仅包含标记原始类型(通过
    .toBranded()
    )和原生Date类型的普通对象。快照绝对不能包含真正的私有或内部数据(如密码哈希或内部安全状态)。

2. Structure of an Entity

2. 实体的结构

Here is the template for a domain entity.
typescript
import { UUID, type BrandedUUID } from "@/shared/domain/value-objects/uuid";
import { FolderName, type BrandedFolderName } from "../value-objects/folderName";

// 1. Snapshot definition (No private fields like password hashes)
export type FolderSnapshot = {
  folderId: BrandedUUID;
  folderName: BrandedFolderName;
  createdAt: Date;
  updatedAt: Date;
};

export class Folder {
  // 2. Private properties, strictly typed with VOs
  private readonly folderId: UUID;
  private folderName: FolderName;
  private readonly createdAt: Date;
  private updatedAt: Date;

  // 3. Object parameter constructor
  constructor(params: {
    folderId: UUID;
    folderName: FolderName;
    createdAt: Date;
    updatedAt: Date;
  }) {
    this.folderId = params.folderId;
    this.folderName = params.folderName;
    this.createdAt = params.createdAt;
    this.updatedAt = params.updatedAt;
  }

  // 4. Domain behavior (mutates state with intent)
  rename(newName: FolderName): void {
    // Optionally apply business rules here before mutation
    this.folderName = newName;
    this.updatedAt = new Date();
  }

  // 5. Explicit Getters
  getId(): UUID {
    return this.folderId;
  }

  getName(): FolderName {
    return this.folderName;
  }

  getCreatedAt(): Date {
    return this.createdAt;
  }

  getUpdatedAt(): Date {
    return this.updatedAt;
  }

  // 6. Snapshot builder
  toSnapshot(): FolderSnapshot {
    return {
      folderId: this.folderId.toBranded(),
      folderName: this.folderName.toBranded(),
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}
以下是领域实体的模板。
typescript
import { UUID, type BrandedUUID } from "@/shared/domain/value-objects/uuid";
import { FolderName, type BrandedFolderName } from "../value-objects/folderName";

// 1. Snapshot definition (No private fields like password hashes)
export type FolderSnapshot = {
  folderId: BrandedUUID;
  folderName: BrandedFolderName;
  createdAt: Date;
  updatedAt: Date;
};

export class Folder {
  // 2. Private properties, strictly typed with VOs
  private readonly folderId: UUID;
  private folderName: FolderName;
  private readonly createdAt: Date;
  private updatedAt: Date;

  // 3. Object parameter constructor
  constructor(params: {
    folderId: UUID;
    folderName: FolderName;
    createdAt: Date;
    updatedAt: Date;
  }) {
    this.folderId = params.folderId;
    this.folderName = params.folderName;
    this.createdAt = params.createdAt;
    this.updatedAt = params.updatedAt;
  }

  // 4. Domain behavior (mutates state with intent)
  rename(newName: FolderName): void {
    // Optionally apply business rules here before mutation
    this.folderName = newName;
    this.updatedAt = new Date();
  }

  // 5. Explicit Getters
  getId(): UUID {
    return this.folderId;
  }

  getName(): FolderName {
    return this.folderName;
  }

  getCreatedAt(): Date {
    return this.createdAt;
  }

  getUpdatedAt(): Date {
    return this.updatedAt;
  }

  // 6. Snapshot builder
  toSnapshot(): FolderSnapshot {
    return {
      folderId: this.folderId.toBranded(),
      folderName: this.folderName.toBranded(),
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

3. Creating vs Reconstituting

3. 创建与重构

Entities don't return
Result
from constructors because the primitives passed to them are already validated Value Objects (which handled the
neverthrow
validation).
  • Infrastructure Layer: Reads primitives from the DB, calls
    UUID.from()
    ,
    FolderName.from()
    , etc. If valid, it MUST pass them into the
    new <Entity>({ ... })
    constructor.
  • Use Cases: Creates entirely new identifiers
    UUID.random()
    , takes validated input from the Server Action, and instantiates the Entity to persist it.
实体的构造函数不会返回
Result
,因为传入的原始类型已经是经过验证的值对象(由
neverthrow
处理验证逻辑)。
  • 基础设施层:从数据库读取原始类型,调用
    UUID.from()
    FolderName.from()
    等方法。如果验证通过,必须将它们传入
    new <Entity>({ ... })
    构造函数。
  • 用例层:创建全新的标识符
    UUID.random()
    ,从Server Action获取经过验证的输入,然后实例化实体以进行持久化。

4. Data Boundaries (Snapshots & Serialization)

4. 数据边界(快照与序列化)

Entities MUST NOT be returned directly from Server Actions to the Client, as class instances lose their methods across the RSC (React Server Component) / Client boundary.
When an Entity needs to be serialized for the client, Server Actions MUST extract data using the entity's
.toSnapshot()
method. The
Snapshot
type is a structured plain object explicitly mapping Value Objects down to Branded Primitives.
Crucially, a Snapshot MUST NEVER include restricted private fields (like cryptographic hashes or internal server configurations). The Snapshot serves simultaneously as the serialization format and the public boundary type.
不得直接从Server Action向客户端返回实体,因为类实例在React Server Component(RSC)与客户端的边界传输时会丢失其方法。
当需要将实体序列化给客户端时,Server Action必须使用实体的
.toSnapshot()
方法提取数据。
Snapshot
类型是一个结构化的普通对象,显式地将值对象映射为标记原始类型。
关键的是,快照绝对不能包含受限的私有字段(如加密哈希或内部服务器配置)。快照同时作为序列化格式和公共边界类型。

5. Aggregate Roots

5. 聚合根

If an entity controls child entities (e.g., a
Workspace
controlling
TeamMembers
), the workspace is the Aggregate Root.
  • All modifications to child entities MUST route through the Aggregate Root's methods.
  • Repositories MUST ONLY be built for Aggregate Roots, not for child entities directly.
如果一个实体控制子实体(例如
Workspace
控制
TeamMembers
),那么该工作区就是聚合根。
  • 对子实体的所有修改必须通过聚合根的方法进行。
  • 仓库(Repositories)只能为聚合根构建,不能直接为子实体构建。