
Ddd Domain Designer
- 17 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Turns event-storming output into code-ready domain models via a 6-step process covering aggregates, bounded contexts, entities, value objects, and domain events.
About
Guides the full DDD domain-design pipeline from event storming to code-ready aggregates and bounded contexts. A developer uses it to design domain models, identify aggregates, and map DO/DTO/VO/PO.
- 6-step modeling from vision to development
- Aggregate design 5-principles and 6-rules
Ddd Domain Designer by the numbers
- 17 all-time installs (skills.sh)
- Ranked #3,475 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-domain-designerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Turns event-storming output into code-ready domain models via a 6-step process covering aggregates, bounded contexts, entities, value objects, and domain events.
Files
DDD Domain Designer
Domain design full workflow — from event storming results to code-ready domain models, aggregates, and bounded contexts.
Workflow
This skill covers the complete domain design pipeline from business understanding to code-ready models:
Business Requirements → [Event Storming] → 6-Step Process → Code-Ready Domain ModelsStep 1: 产品愿景 — FOR [用户] WHO [需求]…UNLIKE [替代方案] 模板。产出:愿景墙。 Step 2: 场景分析 — 梳理用户旅程,捕获领域事件(橙色)/命令(蓝色)/角色(黄色)。产出:事件时间线。 Step 3: 领域建模 — 提取实体/值对象 → 聚类为聚合 → 划分限界上下文。产出:聚合清单。 Step 4: 微服务拆分 — 评估各BC独立部署价值。一个BC = 一个潜在微服务。产出:上下文映射图。 Step 5: 详细设计 — 确定聚合根、实体、值对象、领域事件(过去式命名)、不变式。 Step 6: 开发测试 — 聚合为单位组织开发测试,每聚合一个Repository接口。
详见 references/ddd-tactical.md
When to Use
Use when: designing domain models from event storming outputs, identifying aggregates/bounded contexts, mapping DO/DTO/VO/PO, selecting value object persistence strategies, designing domain events and invariants.
Not for: no event storming input → ddd-event-storming | basic DDD concepts → ddd-architecture-awesome | review existing models → ddd-code-reviewer | generate API → ddd-api-designer | simple CRUD (skip domain modeling)
Boundary
擅长:
- 从事件风暴结果到领域模型设计(聚合、限界上下文、实体/值对象)
- 聚合设计 6 原则落地:一致性边界、小聚合、ID引用
- 限界上下文划分和 7 种上下文映射模式
- 领域对象→代码对象映射(DO/DTO/VO/PO)
- 值对象持久化策略选择(Inline/JSON/Embeddable)
- 领域事件设计与发布策略、领域不变式识别
需条件:已有事件风暴输出或明确业务需求、已选定架构模式、有统一语言(UL)、领域专家可复核。
不适用:无事件风暴结果→ddd-event-storming | 只需基础概念→ddd-architecture-awesome | 审查已有模型→ddd-code-reviewer | 从模型生成API→ddd-api-designer | 简单CRUD→跳过领域建模
Audience
This skill is designed for: Backend developers (implementing DDD architectures), Software architects (evaluating and selecting patterns), Tech leads (reviewing team implementations), and DDD beginners (learning domain-driven design fundamentals).
适用用户
| 用户类型 | 前置知识 |
|---|---|
| 后端开发 | 了解 DDD 基础概念(实体/值对象/聚合) |
| 架构师 | 有分布式系统经验,了解领域驱动设计 |
| 技术负责人 | 理解 DDD 战略设计 |
| DDD 初学者 | 建议先看 ddd-architecture-awesome |
事件风暴驱动 6 步流程
详见 references/ddd-tactical.md、references/clean-ddd-hexagonal-tactical.md
Aggregate Rules / 聚合设计五步法 + 六原则
五步法: (1)识别实体/值对象 → (2)一致性边界分组 → (3)选择聚合根 → (4)定义不变式 → (5)领域专家复核
六原则:
| # | 原则 | 说明 | 审查要点 |
|---|---|---|---|
| 1 | 一致性边界内建模真正的不变条件 | 聚合封装业务规则 | 不变式是否可能被外部破坏? |
| 2 | 设计小聚合 | 大聚合导致并发瓶颈 | 实体 > 5 个?有无 N+1 隐患? |
| 3 | 通过唯一标识引用其他聚合 | 聚合间只记ID,不持对象引用 | 存在直接对象引用? |
| 4 | 边界外使用最终一致性 | 一个事务只改一个聚合 | 跨聚合用领域事件异步处理 |
| 5 | 通过应用层实现跨聚合调用 | 领域服务不直接跨聚合 | 应用层承担编排职责? |
| 6 | 适合自己才是最好的 | 可突破原则但需记录理由 | ADR记录决策理由 |
详见 references/advanced-tactical-patterns.md、references/domain-invariants.md
限界上下文划分
划分依据: 语言变化点、团队组织边界(康威定律)、变更频率差异、独立部署需求、业务能力域(核心/支撑/通用)。
7种映射模式: Partnership(高耦合) | Shared Kernel(高) | Customer-Supplier(中) | Conformist(中) | Anti-Corruption Layer(低) | Open Host Service(低) | Published Language(低)
详见 references/bounded-context-mapping.md
领域对象 → 代码对象映射
四类对象: PO(持久化→Infrastructure) | DO(领域对象→Domain, 充血模型) | DTO(传输对象→Interface/App) | VO(视图对象→Interface)
转换链: VO ↔ DTO ↔ DO ↔ PO (Repository托管DO↔PO; Assembler转换DO→DTO; BFF组装DTO→VO)
详见 references/code-model-mapping.md
与 ddd-event-storming 的关系
推荐流程: event-storming(探索) → domain-designer(详细设计) → (架构Skill)(落地实现)
Gotchas
1. 聚合过大(10+实体) → 小聚合原则,推荐1-5个 2. 缺少一致性边界(单事务改2聚合根) → 单事务只改一个聚合 3. 值对象被当成实体(Address有ID) → 属性相等则为VO,设为不可变 4. 聚合根ID用自增ID → 应用UUID或业务编号(领域事件/分布式唯一标识需要) 5. 领域事件命名不规范 → 过去式("OrderPaid"非"PayOrder") 6. 领域层依赖框架注解 → Domain层纯POJO,零框架依赖 7. 直接对象引用跨聚合 → 只持聚合B的ID 8. 跨聚合事务 → 最终一致性+发件箱模式(Outbox Pattern) 9. 聚合根行为泄漏到Service → 业务逻辑内聚在聚合根方法,Service只做编排 10. 忽略统一语言(UL) → 代码术语与业务语言保持一致
详见 references/ddd-tactical.md
验证指南
自检清单: 不变式文档化? | 聚合根ID用业务标识? | 跨聚合ID引用? | 单事务改一聚合? | 值对象不可变? | 实体充血模型? | 领域事件过去式? | 每事件有发布者/消费者? | VO持久化策略选定? | BC边界明确? | 统一语言建立? | 聚合大小合理(≤5实体)?
FAQ
- 聚合含多少实体? 1-5个,超5个评估拆分。
- 聚合根可以是VO? 不可,聚合根必须是实体(有独立生命周期和全局唯一ID)。
- 领域服务 vs 应用服务? 领域服务:业务规则(如计算折扣)。应用服务:技术编排(如创建订单后通知)。
- BC = 微服务? 理想对应一个微服务,可根据团队/部署频率调整。BC至少是模块边界。
- 实体 vs VO 怎么分? 有独立ID→实体。不可变且属性相等→VO。不满足则重新建模。
- 跨聚合查询怎么做? 应用层组合Repository查询,或在CQRS查询侧创建读模型/物化视图。
- 值对象一定用类封装? 简单值可用基础类型;有验证/行为的建议类封装(Email, Money)。
- 没有领域专家怎么办? 从资深业务人员/产品经理中寻找担任领域专家角色。
- 同实体可在多个BC存在? 可以,但每个BC中有不同含义和属性(如"用户"在权限BC="账号",在订单BC="买家")。
- 发件箱模式(Outbox Pattern)? 同本地事务写领域事件到EventOutbox表,后台进程读取发布到MQ,确保至少一次投递。
Keywords
DDD domain design, 领域建模, aggregate design, 聚合设计, bounded context, 限界上下文, entity, value object, 值对象, domain event, 领域事件, event storming, aggregate root, 聚合根, consistency boundary, 一致性边界, context mapping, 上下文映射, Anti-Corruption Layer, 防腐层, ubiquitous language, 代码映射, DO DTO VO PO, 六边形架构, 领域服务, 聚合反模式, 领域不变式, 持久化策略, 最终一致性, outbox pattern, CQRS, BC划分决策树
References
| File | Purpose |
|---|---|
| references/clean-ddd-hexagonal-strategic.md | DDD 战略设计—限界上下文、上下文映射、集成模式 |
| references/clean-ddd-hexagonal-tactical.md | DDD 战术设计—Entity/VO/Aggregate/Repository/Domain Event |
| references/ddd-tactical.md | 战术模式补充—聚合内部构造、实体/值对象代码模板 |
| references/code-model-mapping.md | 代码模型目录结构、对象映射表、分层策略 |
| references/advanced-tactical-patterns.md | 进阶战术—Factory/Specification/Domain Service |
| references/domain-event-catalog.md | 领域事件设计指南—分类、数据结构、发布策略 |
| references/bounded-context-mapping.md | 7种映射模式详解—决策树、Mermaid模板、代码示例 |
| references/persistence-strategies.md | 值对象持久化策略—Inline/JSON/Embeddable决策树 |
| references/domain-invariants.md | 领域不变式设计—分类、实现模式、规格模式、文档模板 |
| references/guides/partme-13-code-model-1.md | 代码模型(上)—DDD微服务代码模型设计 |
| references/guides/partme-14-code-model-2.md | 代码模型(下)—领域模型与代码模型一致性保证 |
Examples
| File | Domain |
|---|---|
| examples/ecommerce-domain.md | 电商平台 |
| examples/banking-domain.md | 银行系统 |
| examples/insurance-domain.md | 保险业务 |
| examples/order-fulfillment-domain.md | 订单履约 |
| examples/healthcare-domain.md | 医疗健康 |
| examples/payment-domain.md | 支付系统 |
| examples/logistic-domain.md | 物流运输 |
Security & Safety
This skill is pure documentation. It does not collect user data, does not access external services or networks, and contains no executable scripts.
银行领域设计示例
银行系统完整的 DDD 领域设计案例。
1. 产品愿景
FOR 个人用户和企业客户 WHO 需要安全、便捷的银行服务,
OUR 数字银行系统 IS 一个全渠道金融服务平台
THAT 提供账户管理、转账支付、贷款申请和理财服务。
UNLIKE 传统银行网点,OUR product 提供 7×24 在线服务和实时交易处理。
2. 限界上下文划分
| 上下文 | 类型 | 职责 | 核心聚合 |
|---|---|---|---|
| Account | Core | 账户生命周期管理 | Account |
| Transaction | Core | 交易处理与记账 | Transaction, LedgerEntry |
| Payment | Core | 支付指令处理 | PaymentOrder |
| Customer | Supporting | 客户信息管理 | Customer |
| Loan | Supporting | 贷款申请与审批 | LoanApplication, LoanAgreement |
| Risk | Supporting | 风控规则引擎 | RiskAssessment |
| Notification | Generic | 消息通知发送 | Notification |
3. 上下文映射
graph LR
subgraph "Core"
A[Account BC]
T[Transaction BC]
P[Payment BC]
end
subgraph "Supporting"
C[Customer BC]
L[Loan BC]
R[Risk BC]
end
subgraph "Generic"
N[Notification BC]
end
P -->|Partnership| T
T -->|Partnership| A
C -->|Conformist| A
P -->|ACL| EXT[External Payment Gateway]
L -->|Customer-Supplier| R
T -->|Open Host| N4. 聚合设计
Account 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| Account | Aggregate Root | 账户聚合根,管理账户状态 |
| AccountId | Value Object | UUID 类型标识 |
| Money | Value Object | 金额(不可变) |
| AccountType | Value Object | 枚举:SAVING/CHECKING/CREDIT |
| AccountStatus | Value Object | ACTIVE/FROZEN/CLOSED |
| OverdraftLimit | Value Object | 透支额度 |
| InterestRate | Value Object | 利率 |
不变式: 1. 账户余额不能低于 -OverdraftLimit 2. FROZEN 账户禁止任何交易 3. CLOSED 账户不可恢复为 ACTIVE
领域事件: AccountCreated, AccountFrozen, AccountClosed, OverdraftLimitChanged
Transaction 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| Transaction | Aggregate Root | 交易记录聚合根 |
| TransactionId | Value Object | 全局唯一交易流水号 |
| TransactionType | Value Object | DEPOSIT/WITHDRAW/TRANSFER |
| Money | Value Object | 交易金额 |
| LedgerEntry | Entity | 分类账分录 |
不变式: 1. 借方总额 = 贷方总额(会计恒等式) 2. 转账交易的 source 与 target 账户不能相同 3. 交易金额必须 > 0
5. 领域事件时间线
客户转账
│
├── TransferInitiated → 风控检查
├── RiskCheckPassed → 扣减源账户
├── SourceAccountDebited → 增加目标账户
├── TargetAccountCredited → 发送通知
└── TransferCompleted → 记录交易明细6. 代码映射
| 领域对象 | 代码对象 | 包路径 |
|---|---|---|
| Account | Account DO | domain/account/entity/Account.java |
| AccountId | AccountId VO | domain/account/valueobject/AccountId.java |
| Transaction | Transaction DO | domain/transaction/entity/Transaction.java |
| Money | Money VO | domain/shared/valueobject/Money.java |
| TransferInitiated | TransferInitiatedEvent | domain/transaction/event/TransferInitiatedEvent.java |
| AccountRepository | AccountRepository | domain/account/repository/AccountRepository.java |
E-Commerce Domain Design Example
A complete worked example of DDD domain design for an e-commerce system.
1. Product Vision (Elevator Pitch)
FOR online shoppers WHO want to buy products conveniently,
OUR e-commerce platform IS an online marketplace
THAT provides seamless ordering, payment, and delivery tracking.
UNLIKE traditional retail, OUR product offers 24/7 availability and doorstep delivery.
2. Bounded Contexts
| Context | Type | Responsibility | Key Aggregates |
|---|---|---|---|
| Order | Core | Order lifecycle management | Order |
| Payment | Core | Payment processing | Payment, Refund |
| Product | Supporting | Product catalog management | Product, Category |
| Inventory | Supporting | Stock management | Inventory, Warehouse |
| Shipping | Supporting | Logistics coordination | Shipment |
| Customer | Generic | Customer profile & auth | Customer |
3. Context Mapping
Order Context ←→ Payment Context (Partnership)
Order Context ←→ Shipping Context (Customer-Supplier: Order is upstream)
Order Context → Inventory Context (Customer-Supplier: Order is upstream)
Order Context → Product Context (Conformist: Order uses Product IDs)
Payment Context → External Gateway (Anti-Corruption Layer)4. Aggregate: Order
Aggregate Root
- Order
- Identity:
OrderId(value object, UUID) - State:
OrderStatus(DRAFT → PAID → SHIPPING → DELIVERED → CANCELLED)
Entities
- OrderItem: product reference, quantity, unit price, subtotal
Value Objects
OrderId: UUID-based identifierMoney: amount + currency (immutable)Address: street, city, postal code, countryOrderStatus: enum with state machine transitionsPhoneNumber: validated phone format
Invariants
1. totalAmount = sum of all OrderItem.subtotal 2. Status transitions: DRAFT→PAID→SHIPPING→DELIVERED (happy path) 3. Payment amount must match totalAmount 4. Order must have at least 1 OrderItem 5. Shipping address required before PAID→SHIPPING transition
Domain Events
| Event | Trigger | Consumers |
|---|---|---|
OrderPlaced | Order created | Inventory (reserve stock), Notification (email) |
OrderPaid | Payment confirmed | Shipping (create shipment), Notification (SMS) |
OrderShipped | Shipment dispatched | Notification (tracking info) |
OrderDelivered | Delivery confirmed | Payment (release funds), Customer (review prompt) |
OrderCancelled | Cancellation | Inventory (release stock), Payment (refund) |
State Machine
DRAFT ──pay()──▶ PAID ──ship()──▶ SHIPPING ──deliver()──▶ DELIVERED
│ │ │
└──cancel()──▶ CANCELLED ◀──cancel()─┘5. Code Object Mapping
| Domain Object | Persistence (PO) | Transfer (DTO) | View (VO) |
|---|---|---|---|
| Order | OrderPO | OrderDTO, OrderSummaryDTO | OrderDetailVO, OrderListVO |
| OrderItem | OrderItemPO | OrderItemDTO | OrderItemVO |
| Money | amount + currency columns | string "$99.00" | string "¥99.00" |
| Address | address JSONB column | AddressDTO | AddressVO |
| OrderId | VARCHAR(36) PK | String | String |
6. Persistence Strategy
| Object | Strategy | Implementation |
|---|---|---|
| Money | Inline columns | DECIMAL(10,2) amount, VARCHAR(3) currency |
| Address | JSON column | PostgreSQL jsonb, JPA @ColumnTransformer |
| OrderItem | Own table with FK | order_items table, cascade save/load |
| PhoneNumber | Single VARCHAR column | Validated on construction |
7. Microservice Splitting
Phase 1 (Monolith):
All contexts in single deployment
Phase 2 (Split Core):
Order + Payment → order-service
Product + Inventory → product-service
Shipping → shipping-service (new)
Phase 3 (Full Microservices):
Each bounded context → independent service
Shared: Customer context (Auth service)医疗健康领域设计示例
医疗健康系统的 DDD 领域设计案例。
1. 产品愿景
FOR 医疗机构和患者 WHO 需要高效、安全的医疗服务管理,
OUR 医疗健康平台 IS 一个全流程智慧医疗系统
THAT 提供预约挂号、电子病历、诊疗管理和药房管理。
UNLIKE 传统纸质病历管理,OUR product 提供电子化诊疗记录和跨院数据共享。
2. 限界上下文划分
| 上下文 | 类型 | 职责 | 核心聚合 |
|---|---|---|---|
| Appointment | Core | 预约挂号管理 | Appointment, Schedule |
| Clinic | Core | 诊疗过程管理 | Consultation, Diagnosis |
| EMR | Core | 电子病历管理 | MedicalRecord, ClinicalNote |
| Pharmacy | Supporting | 药房处方管理 | Prescription, Dispensation |
| Billing | Supporting | 医疗费用结算 | Invoice, Payment |
| Patient | Supporting | 患者信息管理 | Patient |
3. 上下文映射
graph LR
subgraph "Core"
A[Appointment BC]
C[Clinic BC]
E[EMR BC]
end
subgraph "Supporting"
P[Pharmacy BC]
B[Billing BC]
PAT[Patient BC]
end
A -->|Customer-Supplier| C
C -->|Partnership| E
C -->|Customer-Supplier| P
C -->|Customer-Supplier| B
A -->|Conformist| PAT
C -->|Conformist| PAT4. 聚合设计
Appointment 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| Appointment | Aggregate Root | 预约挂号聚合根 |
| AppointmentId | Value Object | 预约编号 |
| AppointmentStatus | Value Object | BOOKED → CHECKED_IN → COMPLETED → CANCELLED |
| TimeSlot | Value Object | 时间段(date, startTime, endTime) |
| PatientInfo | Value Object | 患者摘要(name, patientId) |
| DoctorInfo | Value Object | 医生摘要 |
不变式: 1. 同一医生在同一时段只能有一个预约 2. 一个患者一天内同科室最多预约 1 次 3. 取消预约必须在预约时间 2 小时前
Consultation 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| Consultation | Aggregate Root | 诊疗记录聚合根 |
| ConsultationId | Value Object | 诊疗编号 |
| Diagnosis | Entity | 诊断记录(ICD 编码) |
| VitalSigns | Value Object | 生命体征(temperature, bloodPressure, heartRate) |
| PrescriptionRef | Value Object | 处方引用 |
不变式: 1. 诊疗必须有诊断结论 2. 处方药品总量不超过该药品单次最大剂量 3. 诊疗记录一旦完成不可修改(可追加)
5. 领域事件目录
| 事件 | 触发条件 | 消费者 |
|---|---|---|
| AppointmentBooked | 患者预约成功 | Clinic, Notification |
| AppointmentCheckedIn | 患者到诊 | Consultation |
| DiagnosisCompleted | 诊疗完成 | EMR, Pharmacy, Billing |
| PrescriptionIssued | 处方开具 | Pharmacy |
| MedicationDispensed | 药品发放完毕 | Billing, Notification |
6. 代码映射
| 领域对象 | 代码对象 | 包路径 |
|---|---|---|
| Appointment | Appointment DO | domain/appointment/entity/Appointment.java |
| TimeSlot | TimeSlot VO | domain/appointment/valueobject/TimeSlot.java |
| Consultation | Consultation DO | domain/clinic/entity/Consultation.java |
| VitalSigns | VitalSigns VO | domain/clinic/valueobject/VitalSigns.java |
| DiagnosisCompleted | DiagnosisCompletedEvent | domain/clinic/event/DiagnosisCompletedEvent.java |
| AppointmentRepository | AppointmentRepository | domain/appointment/repository/AppointmentRepository.java |
保险领域设计示例
保险业务完整的 DDD 领域设计案例。
1. 产品愿景
FOR 有保障需求的个人和企业客户 WHO 希望获得合适的保险产品,
OUR 保险核心系统 IS 一个全流程保险业务平台
THAT 覆盖投保、承保、理赔、续保全生命周期。
UNLIKE 传统纸质投保流程,OUR product 提供在线投保和即时承保能力。
2. 限界上下文划分
| 上下文 | 类型 | 职责 | 核心聚合 |
|---|---|---|---|
| Underwriting | Core | 投保、核保、出单 | Proposal, Policy |
| Claim | Core | 理赔处理 | Claim, Assessment |
| Payment | Core | 保费收付 | PremiumPayment |
| Product | Supporting | 产品定义与费率 | Product, CoverageDefinition |
| Customer | Supporting | 客户信息管理 | Customer |
| Agent | Supporting | 代理人管理 | Agent, Commission |
| Reinsurance | Supporting | 再保分摊 | ReinsuranceContract |
3. 上下文映射
graph LR
subgraph "Core"
U[Underwriting BC]
C[Claim BC]
P[Payment BC]
end
subgraph "Supporting"
PR[Product BC]
CU[Customer BC]
A[Agent BC]
RE[Reinsurance BC]
end
U -->|Partnership| P
U -->|Conformist| PR
U -->|Conformist| CU
C -->|Customer-Supplier| U
P -->|Open Host| A
U -->|ACL| EXT[Credit Bureau API]4. 聚合设计
Underwriting 上下文
Proposal 聚合:
| 元素 | 类型 | 说明 |
|---|---|---|
| Proposal | Aggregate Root | 投保单聚合根 |
| ProposalId | Value Object | 投保单号 |
| ProposalStatus | Value Object | DRAFT → SUBMITTED → UNDERWRITING → APPROVED/REJECTED |
| InsuredPerson | Entity | 被保人信息 |
| CoverageItem | Entity | 险种项目 |
| Premium | Value Object | 保费计算 |
| RiskFactor | Value Object | 风险因子集合 |
不变式: 1. 总保费 = 各险种保费之和 2. 投保人年龄不能超过产品承保年龄上限 3. 被保人告知必须完整(不能缺失必填告知项) 4. DRAFT 状态的投保单可以修改,SUBMITTED 后不可修改
Policy 聚合:
| 元素 | 类型 | 说明 |
|---|---|---|
| Policy | Aggregate Root | 保单聚合根 |
| PolicyId | Value Object | 保单号 |
| PolicyStatus | Value Object | ACTIVE/LAPSED/SURRENDERED/EXPIRED |
| CoveragePeriod | Value Object | 保障期间(startDate, endDate) |
| Endorsement | Entity | 批单记录 |
不变式: 1. 保单生效日期必须在投保完成日期之后 2. 保障期间内不能重复投保相同的险种 3. 保单状态不允许跳转(如 ACTIVE → SURRENDERED 必须经过校验)
5. 领域事件目录 (Underwriting BC)
| 事件 | 触发条件 | 消费者 |
|---|---|---|
| ProposalSubmitted | 客户提交投保单 | 核保系统 |
| UnderwritingCompleted | 核保审核完成 | 出单系统 |
| PolicyIssued | 保单生成 | 支付系统、客户通知 |
| PremiumPaid | 首期保费缴纳 | 保单激活 |
| PolicyActivated | 保单生效 | 理赔系统 |
| PolicySurrendered | 退保 | 财务系统 |
6. 代码映射
| 领域对象 | 代码对象 | 包路径 |
|---|---|---|
| 投保单 | Proposal DO | domain/underwriting/entity/Proposal.java |
| 保单 | Policy DO | domain/underwriting/entity/Policy.java |
| 保费 | Premium VO | domain/underwriting/valueobject/Premium.java |
| 保障期间 | CoveragePeriod VO | domain/underwriting/valueobject/CoveragePeriod.java |
| 保单已签发 | PolicyIssuedEvent | domain/underwriting/event/PolicyIssuedEvent.java |
| 投保单仓储 | ProposalRepository | domain/underwriting/repository/ProposalRepository.java |
物流领域设计示例
业务概述
物流系统核心业务:订单履约 → 仓储拣货 → 运输配送 → 签收完成。涉及仓库管理、运输调度、配送路线规划等。
限界上下文划分
| 上下文 | 职责 | 聚合 |
|---|---|---|
| Fulfillment | 订单履约执行 | FulfillmentOrder, Dispatch |
| Warehouse | 仓储管理 | Inventory, StockMovement |
| Transportation | 运输管理 | Shipment, RoutePlan |
| Delivery | 末端配送 | DeliveryOrder, DeliveryTask |
| Tracking | 物流追踪 | TrackingRecord |
聚合设计
1. FulfillmentOrder 聚合
| 角色 | 名称 | 类型 | 描述 |
|---|---|---|---|
| 聚合根 | FulfillmentOrder | Entity | 履约单,关联原始订单和物流 |
| 实体 | FulfillmentItem | Entity | 履约商品明细 |
| 值对象 | Address | VO | 收货地址 |
| 领域事件 | FulfillmentCreated | Event | 履约单已创建 |
| 领域事件 | FulfillmentCompleted | Event | 履约已完成 |
不变式:
- FulfillmentItem.quantity ≤ Inventory.availableQuantity
- 一个 FulfillmentOrder 必须关联至少一个 FulfillmentItem
- 状态流转:CREATED → PICKING → PACKED → SHIPPED → DELIVERED
2. Shipment 聚合
| 角色 | 名称 | 类型 | 描述 |
|---|---|---|---|
| 聚合根 | Shipment | Entity | 运单,描述一次运输任务 |
| 实体 | ShipmentStop | Entity | 运输节点(装货/卸货点) |
| 值对象 | GeoLocation | VO | 地理位置(经纬度) |
| 值对象 | TimeWindow | VO | 时间窗口 |
| 领域事件 | ShipmentDispatched | Event | 运单已发车 |
| 领域事件 | ShipmentArrived | Event | 运单已到达 |
不变式:
- Shipment 必须有 ≥ 1 个装货点和 ≥ 1 个卸货点
- 运输路线顺序不能矛盾(按时间窗口排序)
- ShipmentStop 的时间窗口不可重叠
上下文映射
Fulfillment ←[Partnership]→ Warehouse(紧密配合,一起规划拣货)
Fulfillment ←[Customer-Supplier]→ Transportation(Fulfillment 定义运输需求,Transportation 执行)
Transportation ←[Customer-Supplier]→ Delivery(Transportation 将货物送达配送站)
Fulfillment →[OHS]→ Tracking(开放物流数据给 Tracking 上下文做追踪)领域事件流程
订单创建 → FulfillmentCreated
→ Warehouse 接收 → InventoryReserved
→ Picking → StockConfirmed
→ ShipmentCreated → ShipmentDispatched
→ DeliveryOrderCreated → Delivered
→ FulfillmentCompleted代码包结构
logistic-domain/
├── fulfillment/ # 履约聚合
│ ├── entity/ # FulfillmentOrder, FulfillmentItem
│ ├── valueobject/ # Address
│ ├── event/ # FulfillmentCreated, FulfillmentCompleted
│ ├── service/ # FulfillmentSplitService
│ └── repository/ # FulfillmentOrderRepository
├── warehouse/ # 仓储聚合
│ ├── entity/ # Inventory, StockMovement
│ ├── valueobject/ # SkuCode, LocationCode
│ ├── event/ # InventoryReserved, StockConfirmed
│ └── repository/ # InventoryRepository
├── transportation/ # 运输聚合
│ ├── entity/ # Shipment, ShipmentStop
│ ├── valueobject/ # GeoLocation, TimeWindow
│ ├── event/ # ShipmentDispatched, ShipmentArrived
│ └── repository/ # ShipmentRepository
├── delivery/ # 配送聚合
├── tracking/ # 追踪聚合
└── shared/ # 共享值对象
└── valueobject/ # Address, GeoLocation, TimeWindow订单履约领域设计示例
电商订单履约流程的 DDD 领域设计案例。
1. 产品愿景
FOR 电商平台运营团队 WHO 需要高效处理订单履约流程,
OUR 订单履约系统 IS 一个自动化的订单处理引擎
THAT 从订单确认到最终交付的全流程管理。
UNLIKE 手工分单处理,OUR product 提供智能路由和实时跟踪。
2. 限界上下文划分
| 上下文 | 类型 | 职责 | 核心聚合 |
|---|---|---|---|
| Fulfillment | Core | 履约执行与分单 | FulfillmentOrder, Assignment |
| Warehouse | Core | 仓储作业管理 | PickList, Packing |
| Logistics | Supporting | 物流配送管理 | Shipment, Tracking |
| Inventory | Core | 库存分配 | InventoryReservation |
| Returns | Supporting | 退换货处理 | ReturnOrder |
3. 上下文映射
graph LR
subgraph "Core"
F[Fulfillment BC]
W[Warehouse BC]
I[Inventory BC]
end
subgraph "Supporting"
L[Logistics BC]
R[Returns BC]
end
F -->|Partnership| I
F -->|Customer-Supplier| W
W -->|Customer-Supplier| L
R -->|ACL| EXT[Third-party Return Center]
F -->|Open Host| L4. 聚合设计
FulfillmentOrder 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| FulfillmentOrder | Aggregate Root | 履约单聚合根 |
| FulfillmentOrderId | Value Object | 履约单号 |
| FulfillmentStatus | Value Object | PENDING → WAREHOUSE → PICKING → PACKING → SHIPPING → DELIVERED |
| OrderLine | Entity | 履约行项目 |
| ShippingAddress | Value Object | 收货地址 |
| DeliveryPreference | Value Object | 配送偏好 |
| SplitLine | Value Object | 拆单记录 |
不变式: 1. 履约单总行数 = 所有 SplitLine 行数之和 2. 同一履约单只能选择一个仓库发货 3. 配送地址必须在履约区域范围内 4. SHIPPING 状态后不能取消履约
PickList 聚合
| 元素 | 类型 | 说明 |
|---|---|---|
| PickList | Aggregate Root | 拣货单聚合根 |
| PickListId | Value Object | 拣货单号 |
| PickListStatus | Value Object | CREATED → IN_PROGRESS → COMPLETED |
| PickItem | Entity | 拣货项目 |
| WarehouseZone | Value Object | 库区信息 |
不变式: 1. 拣货单所有 PickItem 数量之和 ≤ 库存可用量 2. 同一 PickList 的 PickItem 必须在同一 WarehouseZone 3. COMPLETED 后 PickList 不可修改
5. 领域事件目录
| 领域事件 | 发布者 | 消费者 | 描述 |
|---|---|---|---|
| FulfillmentOrderCreated | Fulfillment | Inventory, Warehouse | 履约单创建,库存分配开始 |
| InventoryReserved | Inventory | Fulfillment, Warehouse | 库存分配完成 |
| PickingCompleted | Warehouse | Fulfillment, Packing | 拣货完成 |
| PackingCompleted | Warehouse | Fulfillment, Logistics | 打包完成 |
| ShipmentDispatched | Logistics | Fulfillment, Notification | 发货出库 |
| DeliveryConfirmed | Logistics | Fulfillment, Returns | 签收确认 |
6. 值对象持久化策略
| 值对象 | 策略 | 实现方式 |
|---|---|---|
| ShippingAddress | JSON Column | PostgreSQL jsonb |
| Money | Inline Columns | amount + currency 列 |
| DeliveryPreference | JSON Column | 灵活扩展字段 |
| FulfillmentStatus | Enum String | 枚举值持久化为字符串 |
| WarehouseZone | Single Column | 区域代码字符串 |
支付领域设计示例
业务概述
支付系统核心流程:用户发起支付 → 资金从用户账户转移到商户账户 → 记录交易流水。支持多种支付方式(余额、银行卡、第三方支付)。
限界上下文划分
| 上下文 | 职责 | 聚合 |
|---|---|---|
| Payment | 支付交易核心 | PaymentOrder, PaymentMethod |
| Settlement | 资金结算与对账 | SettlementAccount, Transaction |
| Refund | 退款处理 | RefundOrder |
| Account | 账户资金管理 | Account, Ledger |
聚合设计
1. PaymentOrder 聚合
| 角色 | 名称 | 类型 | 描述 |
|---|---|---|---|
| 聚合根 | PaymentOrder | Entity | 支付单,包含支付金额、状态、支付方式 |
| 实体 | PaymentItem | Entity | 支付明细(拆单场景) |
| 值对象 | Money | VO | 金额(币种 + 数值) |
| 值对象 | PaymentResult | VO | 支付结果(流水号、支付时间) |
| 领域事件 | PaymentCompleted | Event | 支付完成通知 |
| 领域事件 | PaymentFailed | Event | 支付失败通知 |
不变式:
- PaymentOrder.amount > 0
- 一个 PaymentOrder 只有一个 PaymentResult
- 状态流转:PENDING → PROCESSING → SUCCESS/FAILED
2. Account 聚合
| 角色 | 名称 | 类型 | 描述 |
|---|---|---|---|
| 聚合根 | Account | Entity | 账户(用户/商户),记录余额 |
| 值对象 | Money | VO | 金额 |
| 领域事件 | AccountDebited | Event | 账户扣款 |
| 领域事件 | AccountCredited | Event | 账户入账 |
不变式:
- Account.balance ≥ 0(不允许透支)
- 扣款金额 ≤ 余额
上下文映射
Account ←[ACL]→ Payment(Account 通过防腐层向 Payment 提供查余额和扣款接口)
Payment ←[ACL]→ Settlement(Payment 通过防腐层向 Settlement 推送交易数据)
Settlement ←[ACL]→ Refund(Refund 通过防腐层调用 Settlement 的退款能力)领域事件流程
用户发起支付 → PaymentOrderCreated
→ 调用 Account 扣款 → AccountDebited
→ PaymentOrder 状态变更为 SUCCESS → PaymentCompleted
→ Settlement 接收事件 → 生成 Transaction
→ 如果退款 → RefundRequested → AccountCredited代码包结构
payment-domain/
├── payment/ # 支付聚合
│ ├── entity/ # PaymentOrder, PaymentItem
│ ├── valueobject/ # Money, PaymentResult
│ ├── event/ # PaymentCompleted, PaymentFailed
│ ├── service/ # PaymentValidationService
│ └── repository/ # PaymentOrderRepository
├── account/ # 账户聚合
│ ├── entity/ # Account
│ ├── valueobject/ # Money
│ ├── event/ # AccountDebited, AccountCredited
│ └── repository/ # AccountRepository
├── settlement/ # 结算聚合
│ └── ...
├── refund/ # 退款聚合
│ └── ...
└── shared/ # 共享值对象
└── valueobject/ # Money, Currency进阶战术模式 — Factory、Specification、Domain Service
Factory(工厂)
封装复杂的聚合/实体创建逻辑。
何时使用
- 创建逻辑复杂
- 需要在创建时强制不变式
- 需要构建对象图
模式
public interface OrderFactory {
Order createFromCart(Cart cart, Customer customer);
}
public class OrderFactoryImpl implements OrderFactory {
private final PricingService pricingService;
public Order createFromCart(Cart cart, Customer customer) {
if (cart.isEmpty()) throw new IllegalArgumentException("Cart is empty");
Order order = Order.create(customer.getId());
for (CartItem cartItem : cart.getItems()) {
order.addItem(cartItem.getProductId(),
Quantity.of(cartItem.getQuantity()), cartItem.getUnitPrice());
}
if (customer.getDefaultAddress() != null) {
order.setShippingAddress(customer.getDefaultAddress());
}
return order;
}
}Specification(规约模式)
封装可组合的业务规则,用于查询或验证。
public interface Specification<T> {
boolean isSatisfiedBy(T candidate);
Specification<T> and(Specification<T> other);
Specification<T> or(Specification<T> other);
Specification<T> not();
}
public class OrderAmountSpec implements Specification<Order> {
private final Money minAmount;
public boolean isSatisfiedBy(Order order) {
return order.getTotal().greaterThan(minAmount);
}
}
public class CustomerVIPSpec implements Specification<Customer> {
public boolean isSatisfiedBy(Customer customer) {
return customer.isVIP();
}
}
// 组合使用
var spec = new OrderAmountSpec(Money.cny(100))
.and(new OrderStatusSpec(OrderStatus.DRAFT));
var eligibleOrders = orderRepo.findAll().stream()
.filter(spec::isSatisfiedBy).toList();Domain Service(领域服务)
无法自然归属于某个实体或值对象的无状态操作。
何时使用
- 操作涉及多个聚合
- 操作需要外部信息
- 重要的业务逻辑不属于任何一个实体
public interface PricingService {
Money calculateDiscount(Order order, Customer customer);
}
public class PricingServiceImpl implements PricingService {
public Money calculateDiscount(Order order, Customer customer) {
Money discount = Money.zero();
if (order.itemCount() > 10) {
discount = discount.add(order.getTotal().multiply(0.05));
}
if (customer.isVIP()) {
discount = discount.add(order.getTotal().multiply(0.10));
}
Money maxDiscount = order.getTotal().multiply(0.20);
return discount.isGreaterThan(maxDiscount) ? maxDiscount : discount;
}
}public interface ShippingCostCalculator {
Money calculate(List<OrderItem> items, Address destination);
}
public class ShippingCostCalculatorImpl implements ShippingCostCalculator {
public Money calculate(List<OrderItem> items, Address destination) {
Money baseRate = Money.usd(5.99);
Money perItemRate = Money.usd(1.50);
Money total = baseRate.add(perItemRate.multiply(items.size()));
if (!destination.getCountry().equals("US")) {
total = total.add(Money.usd(15.00));
}
return total;
}
}Aggregate 完整模式(含 reconstitute)
public class Order extends AggregateRoot<OrderId> {
private CustomerId customerId;
private List<OrderItem> items = new ArrayList<>();
private OrderStatus status = OrderStatus.DRAFT;
private Address shippingAddress;
// Factory method
public static Order create(CustomerId customerId) {
Order order = new Order(OrderId.generate(), customerId);
order.addDomainEvent(new OrderCreated(order.getId(), customerId));
return order;
}
// Reconstitution method (for persistence loading)
public static Order reconstitute(OrderId id, CustomerId customerId,
List<OrderItem> items, OrderStatus status, Address shippingAddress) {
Order order = new Order(id, customerId);
order.items = new ArrayList<>(items);
order.status = status;
order.shippingAddress = shippingAddress;
return order;
}
private Order(OrderId id, CustomerId customerId) {
super(id);
this.customerId = customerId;
}
public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
assertCanModify();
if (quantity.value() <= 0) throw new IllegalArgumentException("Quantity must be positive");
items.stream().filter(i -> i.getProductId().equals(productId)).findFirst()
.ifPresentOrElse(i -> i.increaseQuantity(quantity.value()),
() -> items.add(OrderItem.create(productId, quantity, unitPrice)));
addDomainEvent(new OrderItemAdded(getId(), productId, quantity));
}
public void confirm() {
assertStatus(OrderStatus.DRAFT);
if (items.isEmpty()) throw new EmptyOrderException();
if (shippingAddress == null) throw new MissingAddressException();
this.status = OrderStatus.CONFIRMED;
addDomainEvent(new OrderConfirmed(getId(), getTotal()));
}
public void cancel(String reason) {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED)
throw new InvalidStateException("Cannot cancel shipped/delivered order");
this.status = OrderStatus.CANCELLED;
addDomainEvent(new OrderCancelled(getId(), reason));
}
public Money getTotal() {
return items.stream().map(OrderItem::getSubtotal)
.reduce(Money.zero(), Money::add);
}
private void assertCanModify() {
if (status == OrderStatus.CANCELLED)
throw new InvalidStateException("Order is cancelled");
}
private void assertStatus(OrderStatus expected) {
if (status != expected)
throw new InvalidStateException("Expected " + expected + " but was " + status);
}
}限界上下文映射模式详解
Sources: Eric Evans DDD Blue Book, Vaughn Vernon IDDD
7 种上下文映射模式
Partnership(合作关系)
两个团队/上下文相互依赖,共同演化接口。
Team A ←→ Team B
(Order) (Payment)适用: 两个上下文需要紧密配合的业务流程 协议: 共同维护接口测试套件,每次变更双方确认
Shared Kernel(共享内核)
两个上下文共享领域模型的一个子集。
┌─────────────────┐
│ Shared Kernel │ → Customer, Money, OrderId
└─────────────────┘
↑ ↑
Order BC Payment BC适用: 高度协作的团队,共享部分模型 风险: 共享部分的变更需要双方协调
Customer-Supplier(客户-供应商)
上游(Supplier)定义接口,下游(Customer)消费,上游需对下游负责。
Supplier (Order BC) ──→ Customer (Shipping BC)
[定义] [消费]
- OrderStatus - 根据订单状态安排发货
- ShippingAddress - 获取收货地址适用: 有明确上下游关系的上下文 SLA: 上游需保证向下游承诺的能力
Conformist(遵奉者)
下游直接采用上游的模型,不建立防腐层。
Upstream (Product BC) ──→ Conformist (Order BC)
ProductId, Price 直接使用 ProductId适用: 上游模型足够好,不值得做转换 风险: 上游变更直接影响下游
Anti-Corruption Layer(防腐层/ACL)
下游建立转换层,保护自己的领域模型不被上游侵染。
Upstream (Legacy System) ──→ ACL ──→ Order BC
[Old Customer Model] [翻译] [Clean Domain Model]
- custId:String custId→ CustomerId
- addr:String addr → Address(VO)适用: 集成遗留系统、第三方、外部服务 价值: 隔离外部变化、保持领域模型纯净
Open Host Service(开放主机服务)
上游以协议形式提供服务,供多个下游使用。
┌──────────────────────┐
│ Open Host Service │
│ ┌────────────────┐ │
│ │ Payment Gateway│ │
│ └────────────────┘ │
└──────────┬───────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Order BC Shipping BC NotificationPublished Language(发布语言)
使用标准数据格式在上下文间交换数据。
Published Language: 标准化事件格式
┌──────────────────────────────┐
│ { │
│ "eventType": "OrderPlaced",│
│ "version": "1.0", │
│ "payload": {...} │
│ } │
└──────────────────────────────┘适用: 多团队、多技术栈的系统集成 常见实现: JSON Schema, Avro, Protobuf
上下文映射决策树
需要集成的两个上下文是否同属一个团队?
├── 是 → 考虑 Shared Kernel 或 Partnership
└── 否 → 上游是否稳定?
├── 是 → Conformist(简单)或 Open Host Service(多下游)
└── 否 → Anti-Corruption Layer(推荐)Mermaid 上下文映射图模板
graph LR
subgraph "Core Domain"
O[Order Context]
P[Payment Context]
end
subgraph "Supporting"
I[Inventory Context]
S[Shipping Context]
PR[Product Context]
end
subgraph "Generic"
C[Customer Context]
N[Notification Context]
end
O -->|Partnership| P
O -->|ACL| I
O -->|Customer-Supplier| S
O -->|Conformist| PR
O -->|Open Host| N
P -->|ACL| EXT[External Payment Gateway]DDD Strategic Patterns
Sources:
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- DDD Resources — Domain Language (Eric Evans)
- Bounded Context — Martin Fowler
- Domain Driven Design — Martin Fowler
- Anti-Corruption Layer — AWS
- Domain Analysis for Microservices — Microsoft
Overview
Strategic DDD patterns help decompose large systems into manageable parts with clear boundaries. They answer: "How do we divide a complex domain?"
DDD is fundamentally collaborative. The patterns below emerge from conversations, whiteboarding, and modeling sessions with domain experts—not from coding alone.
---
Domain Discovery Techniques
Event Storming
A workshop technique for discovering domain events, aggregates, and bounded contexts.
Orange sticky: Domain Event (past tense: "OrderPlaced")
Blue sticky: Command (imperative: "Place Order")
Yellow sticky: Aggregate (noun: "Order")
Pink sticky: External System / Policy
Purple sticky: Problem / QuestionWorkshop flow: 1. Chaotic exploration — Everyone adds events they know about 2. Timeline ordering — Arrange events chronologically 3. Identify aggregates — Group related events 4. Find boundaries — Where language changes = bounded context boundary 5. Surface problems — Mark unclear areas for follow-up
Context Mapping Workshop
For existing systems, map how bounded contexts currently interact: 1. List all systems/services 2. Identify which team owns each 3. Draw relationships (upstream/downstream) 4. Label relationship types (ACL, Conformist, etc.) 5. Identify pain points in current integrations
---
Ubiquitous Language
The foundation of DDD. A shared vocabulary between developers and domain experts that appears in:
- Code (class names, method names)
- Documentation
- Conversations
- UI labels
Principles
1. One language per bounded context - Different contexts may use the same word differently 2. Code reflects the language - Order.confirm() not Order.setStatus("confirmed") 3. Evolve together - When language changes, code changes
Example
❌ Technical language:
"Set the order entity's status field to 2 and insert a record"
✅ Ubiquitous language:
"Confirm the order and record that it was confirmed"// ❌ Technical, not ubiquitous
class Order {
setStatus(status: number): void { this.status = status; }
}
// ✅ Ubiquitous language
class Order {
confirm(): void {
if (this.status !== OrderStatus.Pending) {
throw new OrderCannotBeConfirmedException(this.id);
}
this.status = OrderStatus.Confirmed;
this.confirmedAt = new Date();
this.addDomainEvent(new OrderConfirmed(this.id));
}
}---
Bounded Contexts
A semantic boundary where a particular domain model applies. Within a bounded context, terms have precise, unambiguous meaning.
Key insight: Polysemy (same word, different meanings) across departments is natural, not a problem. The same term meaning different things in different contexts is expected—"the dominant boundary factor is human culture and language variation." — Martin Fowler
Key Concepts
- Each bounded context has its own ubiquitous language
- Each bounded context has its own model
- The same real-world concept may have different representations in different contexts
Example: E-Commerce System
flowchart TB
subgraph ECommerce["E-Commerce System"]
subgraph Sales["Sales Context"]
SC1["Customer: id, email, preferences"]
SC2["Order: items, total, status"]
end
subgraph Shipping["Shipping Context"]
SH1["Recipient: name, address, phone"]
SH2["Shipment: packages, carrier, trackingNo"]
end
subgraph Billing["Billing Context"]
BC1["Payer: name, billingAddress, paymentMethod"]
BC2["Invoice: lineItems, total, dueDate"]
end
subgraph Catalog["Catalog Context"]
CC1["Product: name, description, price"]
CC2["(no customer concept)"]
end
end
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Catalog fill:#8b5cf6,stroke:#7c3aed,color:white"Customer" means different things:
- Sales: Email, preferences, order history
- Shipping: Delivery address, phone number
- Billing: Payment methods, billing address
Bounded Context = Microservice Boundary
In microservices, each bounded context typically becomes a separate service:
flowchart LR
subgraph Sales["Sales Service"]
S1["Orders DB"]
S2["Order API"]
end
subgraph Shipping["Shipping Service"]
SH1["Shipments DB"]
SH2["Shipping API"]
end
subgraph Billing["Billing Service"]
B1["Invoices DB"]
B2["Billing API"]
end
Sales -->|events| Shipping
Shipping -->|events| Billing
Sales -.->|Integration Events| Events[("Event Bus")]
Shipping -.-> Events
Billing -.-> Events
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white---
Subdomains
Areas of business expertise. Subdomains are discovered, not designed.
Types
| Type | Description | Investment | Example |
|---|---|---|---|
| Core | Competitive advantage | High | Product recommendation engine |
| Supporting | Necessary but not unique | Medium | Order management |
| Generic | Commodity, buy/outsource | Low | Email sending, payments |
Identification Questions
1. What makes us different from competitors? → Core 2. What do we need but isn't our specialty? → Supporting 3. What does everyone need the same way? → Generic
Example: E-Commerce
flowchart TB
subgraph Subdomains["Subdomains"]
subgraph Core["CORE"]
C1["Product search & recommendations"]
C2["Pricing engine"]
C3["Personalization"]
end
subgraph Supporting["SUPPORTING"]
S1["Order management"]
S2["Inventory"]
S3["Customer support"]
S4["Reporting"]
end
subgraph Generic["GENERIC"]
G1["Authentication (Auth0)"]
G2["Payments (Stripe)"]
G3["Email (SendGrid)"]
G4["File storage (S3)"]
end
end
Core --> CoreStrat["Build in-house\nBest developers"]
Supporting --> SuppStrat["Build or buy\nSolid but simple"]
Generic --> GenStrat["Use third-party\nDon't reinvent"]
style Core fill:#ef4444,stroke:#dc2626,color:white
style Supporting fill:#f59e0b,stroke:#d97706,color:white
style Generic fill:#6b7280,stroke:#4b5563,color:white---
Context Mapping
Describes relationships between bounded contexts.
Relationship Patterns
Partnership
Two contexts succeed or fail together. Teams coordinate closely.
flowchart LR
A["Context A"] <-->|"Partnership\nJoint planning\nShared success"| B["Context B"]
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#3b82f6,stroke:#2563eb,color:whiteShared Kernel
Two contexts share a subset of the domain model.
flowchart LR
subgraph A["Context A"]
SK["Shared Kernel"]
end
subgraph B["Context B"]
B1[" "]
end
SK <-->|shared| B
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#10b981,stroke:#059669,color:white
style SK fill:#f59e0b,stroke:#d97706,color:whiteWarning: Shared kernels create coupling. Use sparingly.
Customer-Supplier
Upstream context provides what downstream needs.
flowchart LR
U["Upstream\n(Supplier)"] -->|"Provides API"| D["Downstream\n(Customer)"]
style U fill:#3b82f6,stroke:#2563eb,color:white
style D fill:#10b981,stroke:#059669,color:whiteConformist
Downstream conforms to upstream's model with no negotiation power.
flowchart LR
U["Upstream\n(Dictator)"] -->|"Take it or leave it"| D["Downstream\n(Conformist)\nUses their model"]
style U fill:#ef4444,stroke:#dc2626,color:white
style D fill:#6b7280,stroke:#4b5563,color:whiteExample: Integrating with a third-party API (Stripe, AWS).
Anti-Corruption Layer (ACL)
Translation layer protecting your model from external models.
flowchart LR
Ext["External\nContext"] --> ACL["ACL\nTranslator + Adapter"]
ACL --> Your["Your\nContext"]
ACL -.->|"Translates external\nmodel to your model"| Note[" "]
style Ext fill:#ef4444,stroke:#dc2626,color:white
style ACL fill:#f59e0b,stroke:#d97706,color:white
style Your fill:#10b981,stroke:#059669,color:white
style Note fill:none,stroke:noneUse when:
- Integrating with legacy systems
- Integrating with third-party APIs
- External model is messy or poorly designed
// Anti-Corruption Layer Example
// infrastructure/external/stripe/stripe_payment_acl.ts
import Stripe from 'stripe';
import { Payment, PaymentStatus } from '@/domain/payment/payment';
import { Money } from '@/domain/shared/money';
export class StripePaymentACL {
constructor(private readonly stripe: Stripe) {}
async createPayment(payment: Payment): Promise<string> {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: payment.amount.cents,
currency: payment.amount.currency.toLowerCase(),
metadata: {
orderId: payment.orderId.value,
customerId: payment.customerId.value,
},
});
return paymentIntent.id;
}
translateStatus(stripeStatus: string): PaymentStatus {
const mapping: Record<string, PaymentStatus> = {
'requires_payment_method': PaymentStatus.Pending,
'requires_confirmation': PaymentStatus.Pending,
'requires_action': PaymentStatus.Pending,
'processing': PaymentStatus.Processing,
'succeeded': PaymentStatus.Completed,
'canceled': PaymentStatus.Cancelled,
'requires_capture': PaymentStatus.Authorized,
};
return mapping[stripeStatus] ?? PaymentStatus.Unknown;
}
translateWebhook(event: Stripe.Event): DomainEvent | null {
switch (event.type) {
case 'payment_intent.succeeded':
const intent = event.data.object as Stripe.PaymentIntent;
return new PaymentCompleted(
PaymentId.from(intent.metadata.orderId),
Money.fromCents(intent.amount, intent.currency.toUpperCase())
);
case 'payment_intent.payment_failed':
return null;
default:
return null;
}
}
}Open Host Service / Published Language
Expose a well-defined protocol for integration.
flowchart TB
subgraph OHS["Open Host Service"]
PL["Published Language\n(REST API, gRPC, Events Schema)"]
BC["Your Bounded Context"]
end
PL --> A["Consumer A"]
PL --> B["Consumer B"]
PL --> C["Consumer C"]
style OHS fill:#3b82f6,stroke:#2563eb,color:white
style PL fill:#10b981,stroke:#059669,color:white
style A fill:#6b7280,stroke:#4b5563,color:white
style B fill:#6b7280,stroke:#4b5563,color:white
style C fill:#6b7280,stroke:#4b5563,color:white---
Context Map Diagram
Visual representation of all bounded contexts and their relationships:
flowchart TB
Identity["Identity Context\n(Generic - Auth0)"]
Legacy["Legacy Catalog\n(Legacy)"]
Sales["Sales Context\n(Core)"]
Shipping["Shipping Context\n(Supporting)"]
Billing["Billing Context\n(Supporting)"]
Stripe["Stripe Gateway\n(Generic)"]
Identity -->|Conformist| Sales
Legacy -->|ACL| Sales
Sales <-->|Customer-Supplier| Shipping
Sales -->|Open Host Service| Billing
Billing -->|Conformist| Stripe
style Identity fill:#6b7280,stroke:#4b5563,color:white
style Legacy fill:#9ca3af,stroke:#6b7280,color:white
style Sales fill:#ef4444,stroke:#dc2626,color:white
style Shipping fill:#f59e0b,stroke:#d97706,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Stripe fill:#6b7280,stroke:#4b5563,color:white---
Integration Patterns
Domain Events for Context Integration
interface OrderPlaced {
eventType: 'sales.order.placed';
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
total: number;
shippingAddress: Address;
occurredAt: string;
}
class ShippingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const shipment = Shipment.create({
orderId: ShipmentOrderId.from(event.orderId),
recipient: Recipient.fromAddress(event.shippingAddress),
packages: this.calculatePackages(event.items),
});
await this.shipmentRepository.save(shipment);
}
}
class BillingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const invoice = Invoice.create({
orderId: InvoiceOrderId.from(event.orderId),
customerId: BillingCustomerId.from(event.customerId),
lineItems: event.items.map(item => ({
description: `Product ${item.productId}`,
quantity: item.quantity,
unitPrice: Money.fromNumber(item.price),
})),
total: Money.fromNumber(event.total),
});
await this.invoiceRepository.save(invoice);
}
}Event Schema Registry
Define and version integration event schemas:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.company.com/events/sales/order-placed/v1.json",
"title": "OrderPlaced",
"description": "Published when an order is successfully placed",
"type": "object",
"required": ["eventType", "eventId", "orderId", "occurredAt"],
"properties": {
"eventType": { "const": "sales.order.placed" },
"eventId": { "type": "string", "format": "uuid" },
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"total": { "type": "number", "minimum": 0 },
"occurredAt": { "type": "string", "format": "date-time" }
}
}---
Strategic Design Checklist
- [ ] Identify ubiquitous language terms with domain experts
- [ ] Map subdomains (core, supporting, generic)
- [ ] Define bounded context boundaries
- [ ] Document context map with relationships
- [ ] Design anti-corruption layers for external systems
- [ ] Define integration event schemas
- [ ] Ensure each context has its own data store
DDD Tactical Patterns
Sources:
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- Implementing Domain-Driven Design — Vaughn Vernon (2013)
- Effective Aggregate Design — Vaughn Vernon
- Repository Pattern — Martin Fowler (PoEAA)
Building Blocks Overview
flowchart TB
subgraph Aggregate["Aggregate"]
subgraph AggRoot["Aggregate Root (Entity)"]
E1["Entity"]
E2["Entity"]
VO1["Value Object"]
VO2["Value Object"]
DE["Domain Event"]
end
end
Aggregate -->|Repository| Persistence[("Persistence")]
style Aggregate fill:#3b82f6,stroke:#2563eb,color:white
style AggRoot fill:#10b981,stroke:#059669,color:white
style Persistence fill:#6b7280,stroke:#4b5563,color:white---
Entity
An object with identity that persists through time. Two entities are equal if they have the same identity, regardless of attribute values.
Characteristics
- Has a unique identifier
- Identity persists through lifecycle
- Can change attributes but remains the same entity
- Contains behavior (not just data)
Pattern
abstract class Entity<ID>:
id: ID
equals(other: Entity<ID>) -> bool:
return this.id == other.id
class OrderItem extends Entity<OrderItemId>:
productId: ProductId
quantity: Quantity
unitPrice: Money
static create(productId, quantity, unitPrice) -> OrderItem:
return new OrderItem(
id: OrderItemId.generate(),
productId: productId,
quantity: quantity,
unitPrice: unitPrice
)
increaseQuantity(amount: int):
this.quantity = this.quantity.add(amount)
subtotal() -> Money:
return this.unitPrice.multiply(this.quantity.value)---
Value Object
An object defined by its attributes, not identity. Two value objects are equal if all their attributes are equal.
Characteristics
- Immutable (no setters)
- No identity
- Equality by value (all attributes)
- Self-validating
- Side-effect-free methods
Common Value Objects
| Value Object | Attributes | Validation |
|---|---|---|
| Money | amount, currency | amount >= 0 |
| address | valid email format | |
| Address | street, city, zip, country | required fields |
| DateRange | start, end | start <= end |
| Quantity | value | value > 0 |
Pattern
abstract class ValueObject<Props>:
props: Props
equals(other: ValueObject<Props>) -> bool:
return deepEqual(this.props, other.props)
class Money extends ValueObject<{amount, currency}>:
static create(amount, currency) -> Money:
guard: amount >= 0
guard: currency in SUPPORTED_CURRENCIES
return new Money({amount, currency})
static zero(currency = "USD") -> Money:
return Money.create(0, currency)
add(other: Money) -> Money:
guard: this.currency == other.currency
return Money.create(this.amount + other.amount, this.currency)
subtract(other: Money) -> Money:
guard: this.currency == other.currency
return Money.create(this.amount - other.amount, this.currency)
multiply(factor: number) -> Money:
return Money.create(this.amount * factor, this.currency)
class Email extends ValueObject<{value}>:
static create(email: string) -> Email:
normalized = email.lowercase().trim()
guard: isValidEmailFormat(normalized)
return new Email({value: normalized})
domain() -> string:
return this.value.split("@")[1]
class OrderId extends ValueObject<{value}>:
static generate() -> OrderId:
return new OrderId({value: generateUUID()})
static from(value: string) -> OrderId:
guard: value is not empty
return new OrderId({value})---
Aggregate
A cluster of entities and value objects treated as a single unit for data changes. Has a consistency boundary.
Rules
1. One aggregate root - Single entry point for all modifications 2. Reference by ID only - Aggregates reference others by identity, never by direct object reference 3. Transaction boundary - One aggregate per transaction (eventual consistency between aggregates) 4. Invariants within boundary - Aggregate ensures its own consistency 5. Small aggregates - Prefer smaller over larger
Aggregate Sizing Heuristics
| Metric | Healthy | Warning | Action |
|---|---|---|---|
| Entities per aggregate | 1-5 | 6-10 | >10: Split |
| Lines of code (root) | <500 | 500-1000 | >1000: Split |
| Transaction lock time | <100ms | 100-500ms | >500ms: Split |
| Concurrent modification conflicts | Rare | Occasional | Frequent: Split |
Questions to ask:
- Can parts be eventually consistent? → Separate aggregates
- Do all parts change together? → Same aggregate
- Are there independent lifecycles? → Separate aggregates
Design Guidelines
Good: Small Aggregates
flowchart LR
subgraph Order["Order Aggregate"]
O["Order"]
OI["OrderItems (embedded)"]
end
subgraph Customer["Customer Aggregate"]
C["Customer (standalone)"]
end
subgraph Product["Product Aggregate"]
P["Product (standalone)"]
end
Order -.->|customerId| Customer
Order -.->|productId| Product
style Order fill:#10b981,stroke:#059669,color:white
style Customer fill:#3b82f6,stroke:#2563eb,color:white
style Product fill:#3b82f6,stroke:#2563eb,color:whiteReference by ID only
Bad: God Aggregate
flowchart TB
subgraph GodOrder["Order (God Aggregate)"]
O2["Order"]
C2["Customer (embedded)"]
P2["Products (embedded)"]
SA["ShippingAddress (embedded)"]
end
style GodOrder fill:#ef4444,stroke:#dc2626,color:whiteToo large, too many reasons to change, contention issues
Pattern
abstract class AggregateRoot<ID> extends Entity<ID>:
domainEvents: List<DomainEvent> = []
version: int = 0
addDomainEvent(event: DomainEvent):
this.domainEvents.append(event)
clearDomainEvents():
this.domainEvents = []
class Order extends AggregateRoot<OrderId>:
customerId: CustomerId
items: List<OrderItem> = []
status: OrderStatus
shippingAddress: Address | null
createdAt: DateTime
static create(customerId: CustomerId) -> Order:
order = new Order(
id: OrderId.generate(),
customerId: customerId,
status: DRAFT,
createdAt: now()
)
order.addDomainEvent(OrderCreated{orderId, customerId})
return order
static reconstitute(id, customerId, items, status, ...) -> Order:
order = new Order(...)
return order
addItem(productId, quantity, unitPrice):
guard: status != CANCELLED
guard: status != SHIPPED
guard: quantity > 0
existingItem = this.items.find(i => i.productId == productId)
if existingItem:
existingItem.increaseQuantity(quantity)
else:
this.items.append(OrderItem.create(productId, quantity, unitPrice))
this.addDomainEvent(OrderItemAdded{orderId, productId, quantity})
removeItem(productId):
guard: status != CANCELLED
guard: status != SHIPPED
guard: item exists
this.items.remove(productId)
this.addDomainEvent(OrderItemRemoved{orderId, productId})
confirm():
guard: status == DRAFT
guard: items.length > 0
guard: shippingAddress != null
this.status = CONFIRMED
this.addDomainEvent(OrderConfirmed{orderId, total})
ship(trackingNumber):
guard: status == CONFIRMED
this.status = SHIPPED
this.addDomainEvent(OrderShipped{orderId, trackingNumber})
cancel(reason: string):
guard: status not in [SHIPPED, DELIVERED]
this.status = CANCELLED
this.addDomainEvent(OrderCancelled{orderId, reason})
total() -> Money:
return this.items.reduce((sum, item) => sum.add(item.subtotal()), Money.zero())
itemCount() -> int:
return this.items.reduce((sum, item) => sum + item.quantity.value, 0)---
Repository
Provides collection-like access to aggregates. Abstracts persistence.
Rules
1. One repository per aggregate - Not per entity or table 2. Domain interface - Interface in domain, implementation in infrastructure 3. Aggregate-focused - Save/load entire aggregates 4. No query logic - Complex queries belong in separate read models
Pattern
interface OrderRepository:
findById(id: OrderId) -> Order | null
findByCustomerId(customerId: CustomerId) -> List<Order>
save(order: Order)
delete(order: Order)
nextId() -> OrderId
interface Repository<T extends AggregateRoot<ID>, ID>:
findById(id: ID) -> T | null
save(aggregate: T)
delete(aggregate: T)Common Mistakes
Wrong: Repository per entity
interface OrderItemRepository:
findByOrderId(orderId) -> List<OrderItem>
save(item: OrderItem)Wrong: Query methods in repository
interface OrderRepository:
findByStatus(status) -> List<Order>
findByDateRange(start, end)
countByCustomer(customerId)Correct: Aggregate-focused + separate read model
interface OrderRepository:
findById(id: OrderId) -> Order | null
save(order: Order)
interface OrderReadModel:
findByStatus(status) -> List<OrderSummaryDTO>
findByDateRange(start, end) -> List<OrderSummaryDTO>
countByCustomer(customerId) -> int---
Domain Event
Records something significant that happened in the domain.
Characteristics
- Immutable
- Past tense naming (
OrderPlaced, notPlaceOrder) - Contains data needed by consumers
- Timestamp when it occurred
Pattern
abstract class DomainEvent:
eventId: string = generateUUID()
occurredAt: DateTime = now()
abstract eventType: string
abstract toPayload() -> Map
class OrderCreated extends DomainEvent:
eventType = "order.created"
orderId: OrderId
customerId: CustomerId
toPayload():
return {orderId: orderId.value, customerId: customerId.value}
class OrderConfirmed extends DomainEvent:
eventType = "order.confirmed"
orderId: OrderId
total: Money
toPayload():
return {orderId: orderId.value, total: {amount, currency}}
class OrderShipped extends DomainEvent:
eventType = "order.shipped"
orderId: OrderId
trackingNumber: TrackingNumber---
Domain Service
Stateless operations that don't naturally fit within an entity or value object.
When to Use
- Operation involves multiple aggregates
- Operation requires external information
- Significant business logic that doesn't belong to one entity
Pattern
interface PricingService:
calculateDiscount(order: Order, customer: Customer) -> Money
class PricingServiceImpl implements PricingService:
calculateDiscount(order, customer) -> Money:
discount = Money.zero()
if order.itemCount() > 10:
discount = discount.add(order.total().multiply(0.05))
if customer.isVIP:
discount = discount.add(order.total().multiply(0.10))
maxDiscount = order.total().multiply(0.20)
return min(discount, maxDiscount)
interface ShippingCostCalculator:
calculate(items: List<OrderItem>, destination: Address) -> Money
class ShippingCostCalculatorImpl implements ShippingCostCalculator:
calculate(items, destination) -> Money:
baseRate = Money.create(5.99, "USD")
perItemRate = Money.create(1.50, "USD")
total = baseRate.add(perItemRate.multiply(items.length))
if destination.country != "US":
total = total.add(Money.create(15.00, "USD"))
return total---
Factory
Encapsulates complex aggregate/entity creation.
When to Use
- Creation logic is complex
- Need to enforce invariants during creation
- Need to create object graphs
Pattern
interface OrderFactory:
createFromCart(cart: Cart, customer: Customer) -> Order
class OrderFactoryImpl implements OrderFactory:
pricingService: PricingService
createFromCart(cart, customer) -> Order:
guard: not cart.isEmpty
order = Order.create(customer.id)
for cartItem in cart.items:
order.addItem(
cartItem.productId,
Quantity.create(cartItem.quantity),
cartItem.unitPrice
)
if customer.defaultAddress:
order.setShippingAddress(customer.defaultAddress)
return order---
Specification Pattern
Encapsulates business rules for querying or validation.
interface Specification<T>:
isSatisfiedBy(candidate: T) -> bool
and(other: Specification<T>) -> Specification<T>
or(other: Specification<T>) -> Specification<T>
not() -> Specification<T>
class OrderIsShippable extends Specification<Order>:
isSatisfiedBy(order: Order) -> bool:
return order.status == CONFIRMED and order.items.length > 0
class CustomerHasGoodStanding extends Specification<Customer>:
isSatisfiedBy(customer: Customer) -> bool:
return customer.paymentHistory.all(p => p.status == COMPLETED)
class CompositeSpecification<T> extends Specification<T>:
rules: List<Specification<T>>
isSatisfiedBy(candidate: T) -> bool:
return this.rules.all(r => r.isSatisfiedBy(candidate))领域对象 → 代码对象映射表
代码模型目录结构
{project}-domain/
├── {aggregate}/
│ ├── entity/ # 实体 + 聚合根
│ ├── valueobject/ # 值对象
│ ├── event/ # 领域事件
│ ├── service/ # 领域服务
│ └── repository/ # 仓储接口领域对象与代码对象映射
| 领域对象 | 代码对象 | 包路径 |
|---|---|---|
| 订单聚合根 | Order | domain/order/entity/Order.java |
| 订单项实体 | OrderItem | domain/order/entity/OrderItem.java |
| 金额值对象 | Money | domain/order/valueobject/Money.java |
| 订单状态 | OrderStatus | domain/order/valueobject/OrderStatus.java |
| 订单已支付事件 | OrderPaidEvent | domain/order/event/OrderPaidEvent.java |
| 订单仓储接口 | OrderRepository | domain/order/repository/OrderRepository.java |
| 订单领域服务 | OrderPricingService | domain/order/service/OrderPricingService.java |
代码模型设计原则
1. 包结构按聚合组织,不是按层组织(domain/order/ 下包含该聚合的所有对象) 2. 充血模型:实体类包含业务行为方法 3. 聚合间通过 ID 引用:Order 只持有 CustomerId,不持有 Customer 对象 4. 仓储接口在 Domain 定义:只定义接口签名,不含实现 5. 领域事件命名过去式:OrderPaid, OrderCancelled, ProductCreated
各层对象与转换
| 层 | 对象类型 | 命名规范 |
|---|---|---|
| Interface | VO | XxxVO |
| Interface | Request/Response DTO | XxxRequest, XxxResponse |
| Application | Command/Query | XxxCommand, XxxQuery |
| Application | DTO | XxxDTO |
| Domain | DO (Domain Object) | 实体类名(Order, Customer) |
| Infrastructure | PO (Persistent Object) | XxxPO 或 Entity |
松散分层 vs 严格分层
| 策略 | 特点 | 适用 |
|---|---|---|
| 严格分层 | 每层只能调用下一层 | 强制规范,大型项目 |
| 松散分层 | 上层可调用任意下层 | 灵活,中小项目 |
DDD Tactical Patterns
Sources:
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- Implementing Domain-Driven Design — Vaughn Vernon (2013)
- Effective Aggregate Design — Vaughn Vernon
- Repository Pattern — Martin Fowler (PoEAA)
Building Blocks Overview
flowchart TB
subgraph Aggregate["Aggregate"]
subgraph AggRoot["Aggregate Root (Entity)"]
E1["Entity"]
E2["Entity"]
VO1["Value Object"]
VO2["Value Object"]
DE["Domain Event"]
end
end
Aggregate -->|Repository| Persistence[("Persistence")]
style Aggregate fill:#3b82f6,stroke:#2563eb,color:white
style AggRoot fill:#10b981,stroke:#059669,color:white
style Persistence fill:#6b7280,stroke:#4b5563,color:white---
Entity
An object with identity that persists through time. Two entities are equal if they have the same identity, regardless of attribute values.
Characteristics
- Has a unique identifier
- Identity persists through lifecycle
- Can change attributes but remains the same entity
- Contains behavior (not just data)
Pattern
abstract class Entity<ID>:
id: ID
equals(other: Entity<ID>) -> bool:
return this.id == other.id
class OrderItem extends Entity<OrderItemId>:
productId: ProductId
quantity: Quantity
unitPrice: Money
static create(productId, quantity, unitPrice) -> OrderItem:
return new OrderItem(
id: OrderItemId.generate(),
productId: productId,
quantity: quantity,
unitPrice: unitPrice
)
increaseQuantity(amount: int):
this.quantity = this.quantity.add(amount)
subtotal() -> Money:
return this.unitPrice.multiply(this.quantity.value)---
Value Object
An object defined by its attributes, not identity. Two value objects are equal if all their attributes are equal.
Characteristics
- Immutable (no setters)
- No identity
- Equality by value (all attributes)
- Self-validating
- Side-effect-free methods
Common Value Objects
| Value Object | Attributes | Validation |
|---|---|---|
| Money | amount, currency | amount >= 0 |
| address | valid email format | |
| Address | street, city, zip, country | required fields |
| DateRange | start, end | start <= end |
| Quantity | value | value > 0 |
Pattern
abstract class ValueObject<Props>:
props: Props
equals(other: ValueObject<Props>) -> bool:
return deepEqual(this.props, other.props)
class Money extends ValueObject<{amount, currency}>:
static create(amount, currency) -> Money:
guard: amount >= 0
guard: currency in SUPPORTED_CURRENCIES
return new Money({amount, currency})
static zero(currency = "USD") -> Money:
return Money.create(0, currency)
add(other: Money) -> Money:
guard: this.currency == other.currency
return Money.create(this.amount + other.amount, this.currency)
subtract(other: Money) -> Money:
guard: this.currency == other.currency
return Money.create(this.amount - other.amount, this.currency)
multiply(factor: number) -> Money:
return Money.create(this.amount * factor, this.currency)
class Email extends ValueObject<{value}>:
static create(email: string) -> Email:
normalized = email.lowercase().trim()
guard: isValidEmailFormat(normalized)
return new Email({value: normalized})
domain() -> string:
return this.value.split("@")[1]
class OrderId extends ValueObject<{value}>:
static generate() -> OrderId:
return new OrderId({value: generateUUID()})
static from(value: string) -> OrderId:
guard: value is not empty
return new OrderId({value})---
Aggregate
A cluster of entities and value objects treated as a single unit for data changes. Has a consistency boundary.
Rules
1. One aggregate root - Single entry point for all modifications 2. Reference by ID only - Aggregates reference others by identity, never by direct object reference 3. Transaction boundary - One aggregate per transaction (eventual consistency between aggregates) 4. Invariants within boundary - Aggregate ensures its own consistency 5. Small aggregates - Prefer smaller over larger
Aggregate Sizing Heuristics
| Metric | Healthy | Warning | Action |
|---|---|---|---|
| Entities per aggregate | 1-5 | 6-10 | >10: Split |
| Lines of code (root) | <500 | 500-1000 | >1000: Split |
| Transaction lock time | <100ms | 100-500ms | >500ms: Split |
| Concurrent modification conflicts | Rare | Occasional | Frequent: Split |
Questions to ask:
- Can parts be eventually consistent? → Separate aggregates
- Do all parts change together? → Same aggregate
- Are there independent lifecycles? → Separate aggregates
Design Guidelines
Good: Small Aggregates
flowchart LR
subgraph Order["Order Aggregate"]
O["Order"]
OI["OrderItems (embedded)"]
end
subgraph Customer["Customer Aggregate"]
C["Customer (standalone)"]
end
subgraph Product["Product Aggregate"]
P["Product (standalone)"]
end
Order -.->|customerId| Customer
Order -.->|productId| Product
style Order fill:#10b981,stroke:#059669,color:white
style Customer fill:#3b82f6,stroke:#2563eb,color:white
style Product fill:#3b82f6,stroke:#2563eb,color:whiteReference by ID only
Bad: God Aggregate
flowchart TB
subgraph GodOrder["Order (God Aggregate)"]
O2["Order"]
C2["Customer (embedded)"]
P2["Products (embedded)"]
SA["ShippingAddress (embedded)"]
end
style GodOrder fill:#ef4444,stroke:#dc2626,color:whiteToo large, too many reasons to change, contention issues
Pattern
abstract class AggregateRoot<ID> extends Entity<ID>:
domainEvents: List<DomainEvent> = []
version: int = 0
addDomainEvent(event: DomainEvent):
this.domainEvents.append(event)
clearDomainEvents():
this.domainEvents = []
class Order extends AggregateRoot<OrderId>:
customerId: CustomerId
items: List<OrderItem> = []
status: OrderStatus
shippingAddress: Address | null
createdAt: DateTime
static create(customerId: CustomerId) -> Order:
order = new Order(
id: OrderId.generate(),
customerId: customerId,
status: DRAFT,
createdAt: now()
)
order.addDomainEvent(OrderCreated{orderId, customerId})
return order
static reconstitute(id, customerId, items, status, ...) -> Order:
order = new Order(...)
return order
addItem(productId, quantity, unitPrice):
guard: status != CANCELLED
guard: status != SHIPPED
guard: quantity > 0
existingItem = this.items.find(i => i.productId == productId)
if existingItem:
existingItem.increaseQuantity(quantity)
else:
this.items.append(OrderItem.create(productId, quantity, unitPrice))
this.addDomainEvent(OrderItemAdded{orderId, productId, quantity})
removeItem(productId):
guard: status != CANCELLED
guard: status != SHIPPED
guard: item exists
this.items.remove(productId)
this.addDomainEvent(OrderItemRemoved{orderId, productId})
confirm():
guard: status == DRAFT
guard: items.length > 0
guard: shippingAddress != null
this.status = CONFIRMED
this.addDomainEvent(OrderConfirmed{orderId, total})
ship(trackingNumber):
guard: status == CONFIRMED
this.status = SHIPPED
this.addDomainEvent(OrderShipped{orderId, trackingNumber})
cancel(reason: string):
guard: status not in [SHIPPED, DELIVERED]
this.status = CANCELLED
this.addDomainEvent(OrderCancelled{orderId, reason})
total() -> Money:
return this.items.reduce((sum, item) => sum.add(item.subtotal()), Money.zero())
itemCount() -> int:
return this.items.reduce((sum, item) => sum + item.quantity.value, 0)---
Repository
Provides collection-like access to aggregates. Abstracts persistence.
Rules
1. One repository per aggregate - Not per entity or table 2. Domain interface - Interface in domain, implementation in infrastructure 3. Aggregate-focused - Save/load entire aggregates 4. No query logic - Complex queries belong in separate read models
Pattern
interface OrderRepository:
findById(id: OrderId) -> Order | null
findByCustomerId(customerId: CustomerId) -> List<Order>
save(order: Order)
delete(order: Order)
nextId() -> OrderId
interface Repository<T extends AggregateRoot<ID>, ID>:
findById(id: ID) -> T | null
save(aggregate: T)
delete(aggregate: T)Common Mistakes
Wrong: Repository per entity
interface OrderItemRepository:
findByOrderId(orderId) -> List<OrderItem>
save(item: OrderItem)Wrong: Query methods in repository
interface OrderRepository:
findByStatus(status) -> List<Order>
findByDateRange(start, end)
countByCustomer(customerId)Correct: Aggregate-focused + separate read model
interface OrderRepository:
findById(id: OrderId) -> Order | null
save(order: Order)
interface OrderReadModel:
findByStatus(status) -> List<OrderSummaryDTO>
findByDateRange(start, end) -> List<OrderSummaryDTO>
countByCustomer(customerId) -> int---
Domain Event
Records something significant that happened in the domain.
Characteristics
- Immutable
- Past tense naming (
OrderPlaced, notPlaceOrder) - Contains data needed by consumers
- Timestamp when it occurred
Pattern
abstract class DomainEvent:
eventId: string = generateUUID()
occurredAt: DateTime = now()
abstract eventType: string
abstract toPayload() -> Map
class OrderCreated extends DomainEvent:
eventType = "order.created"
orderId: OrderId
customerId: CustomerId
toPayload():
return {orderId: orderId.value, customerId: customerId.value}
class OrderConfirmed extends DomainEvent:
eventType = "order.confirmed"
orderId: OrderId
total: Money
toPayload():
return {orderId: orderId.value, total: {amount, currency}}
class OrderShipped extends DomainEvent:
eventType = "order.shipped"
orderId: OrderId
trackingNumber: TrackingNumber---
Domain Service
Stateless operations that don't naturally fit within an entity or value object.
When to Use
- Operation involves multiple aggregates
- Operation requires external information
- Significant business logic that doesn't belong to one entity
Pattern
interface PricingService:
calculateDiscount(order: Order, customer: Customer) -> Money
class PricingServiceImpl implements PricingService:
calculateDiscount(order, customer) -> Money:
discount = Money.zero()
if order.itemCount() > 10:
discount = discount.add(order.total().multiply(0.05))
if customer.isVIP:
discount = discount.add(order.total().multiply(0.10))
maxDiscount = order.total().multiply(0.20)
return min(discount, maxDiscount)
interface ShippingCostCalculator:
calculate(items: List<OrderItem>, destination: Address) -> Money
class ShippingCostCalculatorImpl implements ShippingCostCalculator:
calculate(items, destination) -> Money:
baseRate = Money.create(5.99, "USD")
perItemRate = Money.create(1.50, "USD")
total = baseRate.add(perItemRate.multiply(items.length))
if destination.country != "US":
total = total.add(Money.create(15.00, "USD"))
return total---
Factory
Encapsulates complex aggregate/entity creation.
When to Use
- Creation logic is complex
- Need to enforce invariants during creation
- Need to create object graphs
Pattern
interface OrderFactory:
createFromCart(cart: Cart, customer: Customer) -> Order
class OrderFactoryImpl implements OrderFactory:
pricingService: PricingService
createFromCart(cart, customer) -> Order:
guard: not cart.isEmpty
order = Order.create(customer.id)
for cartItem in cart.items:
order.addItem(
cartItem.productId,
Quantity.create(cartItem.quantity),
cartItem.unitPrice
)
if customer.defaultAddress:
order.setShippingAddress(customer.defaultAddress)
return order---
Specification Pattern
Encapsulates business rules for querying or validation.
interface Specification<T>:
isSatisfiedBy(candidate: T) -> bool
and(other: Specification<T>) -> Specification<T>
or(other: Specification<T>) -> Specification<T>
not() -> Specification<T>
class OrderOverValueSpec implements Specification<Order>:
minValue: Money
isSatisfiedBy(order) -> bool:
return order.total().amount >= minValue.amount
class OrderHasItemsSpec implements Specification<Order>:
isSatisfiedBy(order) -> bool:
return order.items.length > 0
canShipFree = OrderOverValueSpec(Money.create(100, "USD"))
.and(OrderHasItemsSpec())
if canShipFree.isSatisfiedBy(order):
applyFreeShipping()领域事件目录设计指南
领域事件识别方法
从事件风暴、用户旅程、业务规则中识别领域事件:
1. 动词过去式测试: 事件名称必须是过去时(OrderPlaced、PaymentReceived) 2. 业务影响测试: 事件发生后是否引起后续操作? 3. 独立意义测试: 事件本身包含足够信息让消费者理解发生了什么
领域事件分类
| 类型 | 描述 | 示例 |
|---|---|---|
| 业务事件 | 核心业务流程的关键节点 | OrderSubmitted, PaymentConfirmed |
| 时间事件 | 定时触发的事件 | PaymentTimeout, SubscriptionExpired |
| 外部事件 | 外部系统触发的事件 | ThirdPartyPaymentCallback |
| 状态事件 | 聚合状态变迁 | OrderStatusChanged, PolicyActivated |
领域事件数据结构
public abstract class DomainEvent<T extends DomainEventId> {
private T eventId;
private LocalDateTime occurredOn;
private int version; // 事件 schema 版本
protected DomainEvent() {
this.eventId = DomainEventId.generate();
this.occurredOn = LocalDateTime.now();
this.version = 1;
}
}
public class OrderPlaced extends DomainEvent<OrderPlacedId> {
private OrderId orderId;
private CustomerId customerId;
private Money totalAmount;
private List<OrderItemLine> items;
private LocalDateTime placedAt;
}领域事件发布策略
| 策略 | 模式 | 适用场景 |
|---|---|---|
| 同步发布 | 事件总线(内存) | 同一进程内、同一微服务内部 |
| 异步发布 | 消息队列(Kafka/RabbitMQ) | 跨微服务、跨限界上下文 |
| 事务性发件箱 | 事件先持久化到 DB,再由发件箱投递 | 保证事件发布与业务操作的事务一致性 |
领域事件命名规范
- 使用过去时态:
OrderCreated,PaymentReceived,InventoryReserved - 包含业务含义: 用业务术语而非技术术语
- 层级结构:
{Aggegate}{Action}或{Domain}{EventType}
常见领域事件示例
Order BC:
- OrderCreated # 订单创建
- OrderItemAdded # 添加订单项
- OrderSubmitted # 订单提交
- OrderPaid # 订单支付
- OrderShipped # 订单发货
- OrderDelivered # 订单交付
- OrderCancelled # 订单取消
- OrderRefunded # 订单退款
Payment BC:
- PaymentInitiated # 支付发起
- PaymentConfirmed # 支付确认
- PaymentFailed # 支付失败
- PaymentRefunded # 支付退款
Inventory BC:
- StockReserved # 库存预留
- StockReleased # 库存释放
- StockOut # 出库
- StockIn # 入库领域不变式设计指南
什么是不变式
不变式(Invariant)是聚合内必须始终成立的业务规则。聚合根作为聚合的管理者,负责在每次状态变更后验证所有不变式。
核心原则:聚合封装不变式。边界之外的任何东西都不能破坏聚合内的业务规则。
不变式分类
| 类型 | 描述 | 示例 |
|---|---|---|
| 状态不变式 | 聚合状态必须在合法状态机内 | Order 状态流转:DRAFT → PAID → SHIPPED → DELIVERED |
| 值不变式 | 聚合内数据的取值范围/关系 | OrderItem.quantity > 0 且 OrderItem.price ≥ 0 |
| 组合不变式 | 多个实体间的约束关系 | 已支付订单不能修改商品列表 |
| 总量不变式 | 聚合内数据的累计约束 | 订单总金额 = Σ(商品单价 × 数量) |
| 时间不变式 | 时间维度的约束 | 订单超时未支付自动取消 |
不变式实现模式
1. 方法内校验(最常用)
public class Order extends AggregateRoot<OrderId> {
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
public void addItem(ProductId productId, Money unitPrice, int quantity) {
// 不变式 1:只有 DRAFT 状态的订单可添加商品
if (status != OrderStatus.DRAFT) {
throw new OrderException("只能修改草稿订单");
}
// 不变式 2:数量必须为正
if (quantity <= 0) {
throw new OrderException("数量必须大于 0");
}
// 不变式 3:单价不能为负
if (unitPrice.isNegative()) {
throw new OrderException("单价不能为负");
}
items.add(new OrderItem(productId, unitPrice, quantity));
recalculateTotal();
}
private void recalculateTotal() {
// 不变式 4:总金额 = Σ(单价 × 数量)
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}2. 规格模式(Specification Pattern)
适用于复杂且可复用的不变式。
public class OrderCanBePaidSpecification implements Specification<Order> {
@Override
public boolean isSatisfiedBy(Order order) {
return order.getStatus() == OrderStatus.DRAFT
&& order.getTotalAmount().isPositive()
&& !order.getItems().isEmpty();
}
}
public class Order {
public void pay(Specification<Order> canBePaidSpec) {
if (!canBePaidSpec.isSatisfiedBy(this)) {
throw new OrderException("订单不满足支付条件");
}
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this.id));
}
}3. 领域服务校验
跨多个聚合根的不变式通过领域服务实现。
public class OrderPricingService {
public Money calculateDiscount(Order order, Customer customer) {
// 不变式:VIP 客户折扣不超过 20%,普通客户不超过 10%
if (customer.isVIP() && order.getDiscount().percent() > 20) {
throw new PricingException("VIP 客户折扣不能超过 20%");
}
if (!customer.isVIP() && order.getDiscount().percent() > 10) {
throw new PricingException("普通客户折扣不能超过 10%");
}
// ... 计算逻辑
}
}不变式文档化模板
每个聚合应附带不变式文档:
## Order 聚合不变式
| ID | 类型 | 描述 | 检查时机 | 违反处理 |
|----|------|------|---------|---------|
| INV-01 | 状态 | 只有 DRAFT 状态的订单可修改 | addItem(), removeItem() | OrderException |
| INV-02 | 值 | OrderItem.quantity > 0 | addItem() | IllegalArgumentException |
| INV-03 | 组合 | 已支付订单不可取消(需走退款流程) | cancel() | OrderException |
| INV-04 | 总量 | 订单总金额 = Σ(单价 × 数量) | recalculateTotal() | 无异常(自动修复)|
| INV-05 | 时间 | 订单 30 分钟内未支付自动取消 | 定时任务 | OrderCancelled 事件 |反模式
| 反模式 | 问题 | 修复 |
|---|---|---|
| 不变式在应用层 | 业务规则散落在 Service 中 | 移到聚合根方法 |
| 不变式在数据库 | 仅靠数据库约束保证 | 在领域层显式校验 |
| 不变式缺失 | 聚合无校验逻辑 | 按模板补充不变式文档 |
| 过度校验 | 非关键路径也校验 | 区分 P0/P1/P2 级别 |
值对象持久化策略
概述
值对象(Value Object)在 DDD 中是不变的对象集合,持久化方式直接影响领域模型的纯净度和数据库性能。本参考文档覆盖三种主流持久化策略及其选择决策树。
三种策略对比
| 策略 | 描述 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|---|
| Inline(内嵌) | VO 属性扁平化到实体表 | 单一值对象、属性少 | 查询高效、无 JOIN | 破坏 VO 概念完整性 |
| JSON(序列化大对象) | VO 序列化为 JSON 存入单列 | 多值集合、结构灵活 | 保持 VO 语义、简化表设计 | 不可索引查询、类型安全弱 |
| Embeddable(可嵌入) | ORM 原生嵌入支持 | ORM 框架(JPA/Hibernate) | 语义完整、ORM 原生 | 仅限 ORM 场景 |
决策树
VO 需要独立查询?
├── 是 → 考虑独立表(转为实体)或 JSON(有限查询)
│ ├── 需要 SQL JOIN 查询 → 转为实体 + 独立表
│ └── 仅按主实体查询 → JSON 序列化
└── 否 → 单一值还是集合?
├── 单一值 → Inline 内嵌
│ ├── ORM 可用 → Embeddable(推荐)
│ └── 无 ORM → 扁平化为字段
└── 集合值 → JSON 序列化
├── 固定数量且 ≤ 3 → Inline 多列
└── 可变数量 → JSON 大对象详细策略
1. Inline 内嵌
VO 的所有属性作为实体表的独立列存储。
// VO
public class Address {
private final String street;
private final String city;
private final String zipCode;
// constructor, getters, equals, hashCode
}
// 实体表列:street, city, zip_code
CREATE TABLE person (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
street VARCHAR(200),
city VARCHAR(100),
zip_code VARCHAR(20)
);适用:地址、货币金额(单字段)等单值 VO。
2. JSON 序列化
VO 序列化为 JSON 字符串存入单列。
// VO 集合
public class OrderItem {
private final ProductId productId;
private final Money unitPrice;
private final int quantity;
}
// 实体表列:items TEXT (JSON)
CREATE TABLE order_ (
id BIGINT PRIMARY KEY,
items TEXT
);
-- items: [{"productId":"P001","unitPrice":99.00,"quantity":2}, ...]适用:订单明细、通讯地址列表等可变数量 VO 集合。
3. Embeddable(JPA 示例)
@Embeddable
public class Address {
private String street;
private String city;
private String zipCode;
}
@Entity
public class Person {
@Id
private Long id;
private String name;
@Embedded
private Address address;
}适用:JPA/Hibernate 项目,ORM 原生支持,保持 VO 概念完整。
常见问题
Q: VO 持久化后可以修改吗? A: 不可。VO 在设计上不可变,持久化更新应整体替换而非修改字段。
Q: 多个实体引用同一个 VO 如何存储? A: 每个实体各自存储自己的 VO 副本(值语义,非共享引用)。
Q: JSON 策略如何保证类型安全? A: 使用强类型序列化库(Jackson/FastJSON 配置)结合 DTO 校验。
DDD 代码模型与目录结构
一级目录(按分层)
{项目名}-service/
├── interfaces/ # 用户接口层 — REST Controller, DTO, Assembler
├── application/ # 应用层 — Service 编排, 事件发布/订阅
├── domain/ # 领域层 — 聚合、实体、值对象、领域服务
└── infrastructure/ # 基础层 — DB 实现、配置、工具类二级目录详解
interfaces/
interfaces/
├── controller/ # REST Controller(接收请求 → 调用 AppService)
├── dto/ # 数据传输对象(Request/Response DTO)
└── assembler/ # DTO ↔ Domain 对象转换application/
application/
├── service/ # 应用服务(编排领域服务 + 外部服务)
├── event/
│ ├── publish/ # 事件发布
│ └── subscribe/ # 事件订阅(调用领域层处理)
└── command/ # Command 对象(CQRS 场景)domain/
domain/{聚合名}/ # 每个聚合一个独立包
├── entity/ # 聚合根、实体、值对象(充血模型)
├── service/ # 领域服务(跨实体的业务逻辑)
├── repository/ # 仓储接口(实现在 infrastructure)
└── event/ # 领域事件定义infrastructure/
infrastructure/
├── repository/ # 仓储实现(MyBatis/JPA 映射)
├── config/ # 配置类
├── mq/ # 消息队列
├── cache/ # 缓存
└── util/ # 通用工具类各层代码规范
| 层 | 允许的代码 | 禁止的代码 |
|---|---|---|
| interfaces | Controller, DTO, Assembler | 业务逻辑、数据库操作 |
| application | 服务编排、事务、权限 | 业务 if/else,SQL 查询 |
| domain | 聚合根、实体、VO、领域服务、仓储接口 | Spring 注解、JPA、JDBC |
| infrastructure | 仓储实现、配置、客户端 | 业务规则(Domain 层不处理的) |
聚合边界规则
原则:一个聚合 = 一个独立包 = 一个仓储
禁止:
- 聚合之间直接调用领域服务
- 聚合间传递实体对象(只能传 ID)
允许:
- 通过应用层编排跨聚合操作
- 通过领域事件异步通信微服务演进时的重组策略
当需要拆分聚合为新微服务时:
1. 复制整个 {聚合名}/ 包到新微服务的 domain/
2. 复制仓储实现到新微服务的 infrastructure/
3. 应用层通过事件/RPC 调用新微服务常见错误
| 错误 | 修复 |
|---|---|
聚合包名为 entity/model(技术名) | 改为业务名(order/payment) |
| 领域服务放在 application 层 | 移到 domain/{聚合名}/service/ |
| 仓储实现在 domain 层 | 接口在 domain,实现在 infrastructure |
| Controller 直接调 Repository | Controller → AppService → Repository |
领域对象 → 代码对象映射指南
映射流程
事件风暴领域对象
→ 整理领域对象清单(聚合、实体、事件、命令)
→ 用户故事/领域故事分析
→ 设计代码对象(充血模型实体、值对象、仓储接口)
→ 确定分层归属
→ 建立领域对象 → 代码对象映射表领域对象整理表模板
| 领域模型 | 聚合 | 领域对象 | 领域类型 |
|---|---|---|---|
| 投保模型 | 投保聚合 | 投保单 | 聚合根 |
| 投保模型 | 投保聚合 | 被保人 | 实体 |
| 投保模型 | 投保聚合 | 证件类型 | 值对象 |
| 投保模型 | 投保聚合 | 投保单已创建 | 领域事件 |
| 投保模型 | 投保聚合 | 提交投保 | 命令 |
领域对象 → 代码对象映射表
| 层 | 领域对象 | 领域类型 | 依赖对象 | 包名 | 类名 | 方法名 |
|---|---|---|---|---|---|---|
| Domain | 个人客户 | 聚合根 | 地址, 电话 | domain.customer.entity | Customer | create, update |
| Domain | 地址 | 实体 | 无 | domain.customer.entity | Address | change |
| Domain | 客户类型 | 值对象 | 无 | domain.customer.entity | CustomerType | — |
| Domain | 客户已创建 | 领域事件 | 个人客户 | domain.customer.event | CustomerCreated | — |
| Domain | 创建客户 | 领域服务 | 个人客户, 地址 | domain.customer.service | CustomerDomainService | createCustomer |
| Domain | 客户仓储 | 仓储接口 | 个人客户 | domain.customer.repository | CustomerRepository | save, findById |
| App | 创建客户 | 应用服务 | 领域服务 | application.service | CustomerAppService | createCustomer |
| App | 客户已创建 | 事件发布 | 领域事件 | application.event.publish | CustomerEventPublisher | publish |
| Interface | 创建客户 DTO | DTO | — | interfaces.dto | CreateCustomerRequest | — |
| Interface | 客户 Controller | Controller | — | interfaces.controller | CustomerController | create |
| Infra | 客户仓储实现 | 仓储实现 | — | infrastructure.repository | CustomerRepositoryImpl | save |
服务分层与调用规则
严格分层模式(推荐):
Interface → Application → Domain Service → Entity Method
↓ ↓
Infrastructure Repository (接口在 Domain)
禁止:
Interface → Domain(跨层)
Interface → Infrastructure(跨层)
Application → Entity Method(跨层,需经 Domain Service)实体设计准则
| 准则 | 做法 |
|---|---|
| 充血模型 | 实体内实现业务方法,不只是 getter/setter |
| 值对象 vs 实体 | 有独立生命周期 + 需要查询统计 → 实体;只读不修改 → 值对象 |
| 实体方法暴露 | 实体方法 → 领域服务封装 → 应用服务封装(逐层暴露) |
| 服务去重 | 应用服务反复编排相同领域服务 → 合并为一个领域服务 |
非典型模型处理(无聚合根场景)
场景:客户归并(扫描所有客户,按身份证/电话去重)
→ 无聚合根
处理:
- 仍用聚合概念组织代码(domain.customer.merge/)
- 实体仍设计属性和行为方法
- 设计仓储接口和领域服务
- 只是没有聚合根来管理生命周期
原则:找不到聚合根不影响使用 DDD 其他方法(实体、值对象、仓储、领域服务)常见错误
| 错误 | 修复 |
|---|---|
| 领域对象和数据库表一一对应 | 领域模型驱动,不是数据模型驱动 |
| 所有方法都在实体上 | 跨实体逻辑放入领域服务 |
| Interface 直接调 Repository | 必须经过 Application Service |
| 把领域事件发布放在 Domain 层 | 发布订阅在 App 层,事件定义在 Domain 层 |