在 DDD 架构中,领域聚合根主键的工程化设计思路
一、为什么「聚合根主键」值得单独设计?
在很多项目中,主键往往被当成数据库层的细节:
int identitybigint auto_increment- 或者一个随手用的
Guid
但在 DDD(领域驱动设计) 中,聚合根是领域模型的核心,
而 聚合根的主键 = 领域中“身份”的表达。
如果主键设计得随意,往往会带来这些问题:
- 领域层被数据库实现细节污染
- 业务语义无法体现(
OrderId/AssetId全是Guid) - 跨聚合、跨上下文传参极易出错
- 后期想重构、拆服务成本极高
一句话总结:
主键不是“存储编号”,而是领域身份(Identity)的一部分。
二、DDD 中聚合根主键的几个核心原则
1️⃣ 主键属于「领域概念」,不是数据库概念
在 DDD 中:
- 聚合根存在于 Domain 层
- Domain 层 不应该依赖数据库
- 因此,主键 不能体现数据库生成策略
这意味着:
❌ 不推荐
public int Id { get; set; }
❌ 不推荐
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
这些都把 “怎么存” 强行带进了 “是什么”。
2️⃣ 不要让所有聚合根的主键「长得一样」
很多系统里会看到这种代码:
public class Order
{
public Guid Id { get; private set; }
}
public class Asset
{
public Guid Id { get; private set; }
}
问题在于:
Order.Id和Asset.Id在类型系统里是完全一样的- 编译器无法阻止你把
Asset.Id传给Order - 所有约束只能靠“人记住”
这不是工程化设计,而是祈祷式设计。
三、工程化推荐方案:Strongly Typed ID(强类型主键)
✅ 1️⃣ 为每个聚合根定义专属 ID 类型
public sealed record AssetId(Guid Value);
public sealed record OrderId(Guid Value);
然后在聚合根中使用:
public class Asset : AggregateRoot<AssetId>
{
public AssetId Id { get; private set; }
private Asset() { }
public Asset(AssetId id)
{
Id = id;
}
}
这样做带来的好处
- 编译期类型安全
- 不同聚合根的 ID 无法混用
- 方法签名即业务语义
- Domain 层完全不关心数据库
四、为什么不推荐在 DDD 中使用「自增主键」
❌ 1️⃣ 自增主键是“存储顺序”,不是“业务身份”
自增 ID 的本质:
- 依赖数据库
- 依赖写入顺序
- 在分布式环境下天然不友好
而领域中的身份应该是:
- 与存储方式无关
- 可提前生成
- 可在内存中完成建模与校验
❌ 2️⃣ 自增主键会反向影响领域建模
典型问题包括:
- 聚合创建必须“先落库”
- 领域事件必须等保存后才能发
- 很难做离线建模、测试或并发构造
这与 DDD 的目标是完全相反的。
五、Strongly Typed ID 如何与 EF Core 协作?
这是很多人真正卡住的地方。
1️⃣ 值对象映射(ValueConverter)
builder.Property(x => x.Id)
.HasConversion(
id => id.Value,
value => new AssetId(value)
);
2️⃣ 数据库里依然是 Guid / bigint
- 数据库不需要知道
AssetId - ORM 负责「领域 ↔ 存储」转换
- 领域模型保持纯净
这正是 Infrastructure 层存在的意义。
六、主键生成策略建议
推荐方案
public static class AssetIdGenerator
{
public static AssetId New() => new AssetId(Guid.NewGuid());
}
- 主键在 领域或应用层生成
- 不依赖数据库
- 易测试、易扩展
是否一定要 Guid?
不一定:
- Guid
- Snowflake
- ULID
- 自定义编码(只要不依赖 DB)
关键不是值的形式,而是:领域是否掌控它。
七、聚合根主键设计的一句话总结
在 DDD 架构中, 聚合根主键不是“表的 ID”, 而是“领域身份的类型化表达”。
如果一个系统里:
- 所有聚合根都是
int Id - Domain 层知道自增、序列、数据库策略
- ID 在服务间随便传
那它大概率只是:
“披着 DDD 外衣的 CRUD 系统”。
八、结语
主键设计看似是一个小点, 但它往往决定了:
- 领域是否真正独立
- 架构是否能长期演进
- 团队是否真的理解 DDD
工程化的 DDD,往往从这些“看起来没必要”的设计开始。