限界上下文是业务语义边界而非功能模块,划分依据是业务规则变更一致性;跨上下文禁止共享domain模型,须通过DTO或带上下文前缀的过去时事件通信;聚合根是事务一致性的最小单元,ID需全局唯一且不可变。

Go 微服务里做 DDD 不是套概念,而是用聚合根、限界上下文、领域事件这些机制,把业务逻辑真正锁在服务边界内。否则拆出来的服务只是“物理隔离”,调用链照样绕成麻花,数据一致性依然靠事务硬扛。
限界上下文怎么划,不是看功能模块,而是看语言和职责
很多人一上来就按“用户”“订单”“支付”起服务名,结果发现用户服务要查订单状态、支付服务要改用户余额——这不是拆分,是把单体切成几块再用 RPC 焊回去。
- 真正划分依据是:同一组业务规则下,谁和谁必须一起变更?比如“优惠券发放”和“核销规则”永远共进退,但和“用户注册流程”完全无关,那就该放进同一个限界上下文(如
coupon-context),哪怕它也涉及用户 ID - 检查标准很简单:打开你的
domain/目录,如果一个结构体的字段引用了另一个上下文的实体(比如order.Order直接嵌套payment.Transaction),说明边界已经破了 - 跨上下文通信必须走异步事件或 DTO 转换,禁止直接 import 对方的 domain 包。你可以在
infrastructure/api层写适配器,但绝不能让application层感知对方的领域模型
聚合根不是“主表”,而是事务一致性的最小单元
常见错误是把数据库主键当聚合根 ID,然后往里塞一堆外键关联的子表——这会让 Save() 变成一场分布式事务噩梦。
- 聚合根只包含能保证强一致性的实体和值对象。例如订单聚合根可以含
OrderItem和ShippingAddress(因为下单时必须同时校验库存和地址格式),但绝不含Payment(支付成功与否不影响订单创建是否成立) - 聚合内部修改必须通过方法而非公开字段:用
order.AddItem(product, qty)而不是order.Items = append(order.Items, item)。前者能封装校验逻辑,后者等于把业务规则甩给调用方 - 聚合根 ID 必须全局唯一且不可变,推荐用
uuid.UUID或业务语义 ID(如"ORD-20260630-001"),别用自增 int——后者在分库分表或事件溯源场景下会直接崩
领域事件不是日志,是跨服务协作的契约
很多团队把 OrderCreated 事件当成“我记一笔”,结果消费者收到后自己解析 JSON、手动映射字段、再硬编码处理逻辑——这等于把接口契约写死在消费端,版本一变全挂。
立即学习“go语言免费学习笔记(深入)”;
- 事件结构必须版本化,字段增删需兼容。推荐用 Protobuf 定义
order_created_v1.proto,生成 Go struct,而不是用map[string]interface{}或裸 JSON - 事件发布必须在聚合根方法内完成,且仅在业务操作成功提交后触发。别在 repository.Save() 前发事件,否则可能产生“事件已发但 DB 写失败”的脏状态
- 消费者要基于事件类型路由,而不是字段内容。用
switch event.Type { case "order_created_v1": ... },别写if event.Data["status"] == "paid"——后者会让消费者耦合到生产者的内部字段
DDD 在 Go 微服务里最难的不是写代码,是守住边界:每个 domain/xxx 目录下的代码,必须只依赖本上下文内的类型;所有跨上下文交互,必须经过明确的接口或事件定义。一旦开始为“方便”而绕过这条线,后续的测试成本、重构代价、故障定位难度,都会指数级增长。



















