限界上下文是业务语义边界而非包名,决定概念共存与隔离;跨上下文禁止共享domain模型,须通过dto或带上下文前缀的过去时事件通信;id应为带上下文标识的封装类型(如userid),非数据库主键。

限界上下文不是包名,而是职责边界
Go 里最容易错把 package 当作限界上下文——比如建个 user 包就以为划出了用户上下文。其实限界上下文是业务语义的边界,它决定哪些概念共存、哪些必须隔离。一个上下文里可以有多个 package(如 user/domain、user/application),跨上下文绝不共享 domain 模型或错误类型。
常见错误现象:user.User 被 order 包直接 import 并赋值字段;或者 order.CreateOrder 接收 *user.User 作为参数。这等于把两个上下文的领域语言混在一起,后续改名、拆分、独立部署全会卡住。
- 判断是否越界:看一个结构体/函数是否同时依赖两个上下文的 domain 类型(比如
User和Product) - 跨上下文通信只允许通过 DTO 或事件,且 DTO 必须在调用方定义(不能复用被调用方的 struct)
- 同一业务名词在不同上下文里可以有不同结构和含义,比如
user.Profile在认证上下文里含PasswordHash,在订单上下文里只有Name和Phone
用 Go module 划分物理边界,但别让 go.mod 决定上下文
go.mod 是发布单元,不是领域单元。你可以在一个 repo 里放多个限界上下文,每个上下文对应一个子模块(如 github.com/your/app/user、github.com/your/app/order),但它们共用顶层 go.mod。这样既能保证构建一致性,又避免过早拆库带来的协作成本。
容易踩的坑:go mod init 直接按目录层级起名,结果得到 github.com/your/app/internal/user ——internal 会让其他上下文无法引用其接口,而限界上下文之间本应能通过契约交互。
立即学习“go语言免费学习笔记(深入)”;
- 对外暴露的接口(如
user.Service)必须放在非internal路径下,例如github.com/your/app/user - domain 层禁止 import application 或 infrastructure 层,但 application 层可以 import domain
- 如果真要拆 repo,优先拆的是部署单元(比如独立服务),而不是因为“DDD 要求每个上下文一个仓库”
领域事件命名必须带上下文前缀,且用过去时
事件是限界上下文之间最轻量、最稳定的集成方式,但 Go 里常有人写成 UserCreated 这种泛化名。问题在于:哪个上下文创建的?是注册流程,还是后台导入,还是第三方同步?不带上下文前缀的事件,消费者根本不敢处理。
正确做法是把上下文名嵌进事件名里,比如 UserRegistered(来自 auth 上下文)、UserProfileUpdated(来自 profile 上下文)。注意全部用过去时,表示事实已发生,不可变。
- 事件结构体必须定义在发布它的上下文内,消费者只依赖事件名和字段,不 import 发布方的包
- 不要用
interface{}或 map 做事件载体——序列化/反序列化会丢失类型安全,调试时连字段名都看不到 - 事件总线(如
bus.Publish(&auth.UserRegistered{}))应由 infrastructure 层提供,domain 层只定义事件结构和触发点(如u.Register()内部调用u.recordEvent(...))
聚合根 ID 不是数据库主键,而是上下文内唯一标识符
很多人直接拿 MySQL 的 INT AUTO_INCREMENT 当聚合根 ID,结果跨上下文关联时发现 ID 冲突、语义不清、迁移困难。DDD 中聚合根 ID 是该上下文内的逻辑标识,必须满足:全局可比对、无业务含义、能承载上下文信息。
推荐用 ULID 或 KSUID(而非 UUID),因为它们有序、可读性略好、且不含随机熵导致的分布问题。更重要的是:ID 字符串本身要能体现来源上下文,比如 usr_01H... 、ord_01J...。这不是为了炫技,而是让日志、监控、数据库外键一眼看出归属。
- 禁止在 domain 层使用
int64或uint类型 ID——它们无法携带上下文信息,也难以扩展为分布式 ID - 数据库表主键仍可用自增整数,但 domain 层看到的 ID 必须是封装后的类型(如
type UserID string),并实现String()和Parse() - 不同上下文的 ID 生成逻辑可以不同,但解析规则必须稳定——今天用 ULID,明天就不能突然切到 Snowflake 格式
真正难的不是怎么分上下文,而是当两个上下文都要修改同一个现实对象(比如“用户邮箱”)时,谁有最终解释权、变更如何同步、不一致时以谁为准——这些得靠业务规则定,代码只能 enforce 边界,没法替人做决策。











