限界上下文必须物理隔离为一级目录,如internal/booking;每上下文内含domain、application、infrastructure、interfaces四层;禁止跨上下文import domain/application包,通信仅通过事件或interfaces层api;domain包须纯业务,禁用基础设施类型与标签。

限界上下文必须物理隔离,不能按技术分层建包
很多人一上来就建 internal/domain、internal/application 这种全局分层目录,结果所有业务逻辑挤在同一个包里,后期根本无法拆服务。Go 没有访问修饰符,靠目录结构守边界——每个限界上下文必须是一级目录,比如 internal/booking、internal/payment、internal/customer。
每个上下文内自包含四层:domain(纯结构体+仓储接口)、application(用例函数)、infrastructure(DB/消息队列实现)、interfaces(HTTP/gRPC 入口)。不允许跨上下文 import 对方的 domain 或 application 包,否则边界就破了。
-
internal/booking/domain/cargo.go被internal/handling直接引用 → 编译不报错,但架构已失效 - 跨上下文通信只能走事件(如
BookingConfirmedEvent)或同步 API(通过interfaces层暴露,而非直接调 infra) - 用
go list ./internal/...配合正则检查 import 路径,是 CI 中最有效的边界防护手段
domain 包里禁止出现任何基础设施痕迹
常见错误:在 domain/user/user.go 里加 gorm.Model、bson.ObjectId 或 json:"id" 标签。这等于把数据库 schema 塞进领域模型,后续换 MongoDB 为 TiDB 或加 Redis 缓存时,整个 domain 层要重写。
domain 下只允许基础类型和值对象:
- 实体字段全是
string、int64、time.Time等原生类型 - 值对象必须不可变,比如
Money、Location自带校验和相等性定义 - 仓储接口定义在
domain/xxx/repository.go,但实现必须落在infrastructure/db/xxx_repo.go或infrastructure/mq/event_publisher.go
application 层只做用例编排,不碰技术细节
这个层最容易膨胀成“万能胶”。正确做法是:一个用例 = 一个函数,输入 DTO,输出 DTO,中间只调领域对象方法和仓储接口。
例如 CreateOrder 用例:
- ✅ 调
orderRepo.Save()、eventPublisher.Publish(OrderCreated{...}) - ❌ 不处理 HTTP header、不调
json.Marshal、不写日志、不生成time.Now()(时间应由领域对象内部生成) - ❌ 不做参数校验(交给接口层中间件或 DTO 自验证)
- DTO 必须定义在
application/dto,与 domain 完全解耦;字段命名按业务语义,不是数据库列名
infra 实现必须可插拔,且不能反向依赖 domain
很多团队把 MySQL 初始化逻辑写在 cmd/main.go 里,然后传给 application 层,导致 domain 被迫知道 DB 类型。正确方式是:infra 提供构造函数返回仓储实现,由 DI 工具(如 Wire)组装。
比如:
-
infrastructure/db/mysql/order_repo.go实现domain/order/repository.OrderRepository -
infrastructure/cache/redis/user_cache.go实现domain/user/repository.UserCache - 所有 infra 包 import 路径不能向上跨过
infrastructure目录 —— 如果它 import 了application,说明职责倒置 - 测试时用内存实现(
infrastructure/memory)替换真实 infra,无需启动数据库就能跑通用例
真正难的不是写代码,而是守住那几条目录线。一旦 internal/booking 和 internal/inventory 开始互相 import domain 结构体,DDD 就退化成了带注释的单体。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











