Table of contents
Open Table of contents
TL;DR
国内大厂(腾讯、字节等)在微服务实践中大量借鉴了 Clean Architecture 的思想,但几乎没有团队完整照搬同心圆模型。实际落地是在敏捷迭代压力下,选择性应用依赖反转原则——核心域严格分层,CRUD 边缘服务直接怼。
大厂微服务架构的现实
腾讯/字节的典型做法
国内大厂微服务架构的演进路径大致相同:
第一阶段:单体拆服务(2015-2018)
按业务线拆,每个服务内部结构随意。MVC、三层架构混用,handler 里写 SQL 的情况普遍。问题不在于没有架构,而是每个团队的架构不一样,跨团队协作成本极高。
第二阶段:统一框架(2018-2021)
腾讯推 trpc-go,字节用 Kitex/Hertz。框架统一了通信层(RPC、服务发现、熔断),但框架管不了业务代码的组织方式。很多团队的 service 层变成了 “转发层”——handler 调 service,service 调 dao,每层只做参数透传,分层变成了形式主义。
第三阶段:DDD/Clean Architecture 引入(2021-至今)
核心业务域开始引入领域驱动设计和整洁架构。但落地时做了大量妥协。
实际落地的分层(不是教科书式的)
service/
├── api/ # 协议层(proto 定义、HTTP handler)
├── biz/ # 业务逻辑(≈ usecase + domain 合并)
│ ├── model/ # 领域模型
│ └── service/ # 业务编排
├── data/ # 数据访问(≈ adapter/repository)
└── conf/ # 配置
关键妥协:
- usecase 和 domain 通常合并为
biz层。教科书的四层在微服务粒度下太重——一个微服务本身就是一个 bounded context,再在内部分四层,认知负担大于收益 - interface 定义经常放在
biz层而非独立的 port 包。Go 社区 “small interface” 的习惯让独立 port 包显得多余 - DTO 和 proto message 合并。gRPC 的 protobuf message 既是传输格式也是 DTO,再加一层转换被认为得不偿失
什么时候严格分层,什么时候放松
大厂的经验法则:
| 服务类型 | 架构策略 | 原因 |
|---|---|---|
| 核心域(支付、交易、风控) | 严格 Clean Architecture,domain 层独立 | 业务规则复杂,变更频繁,需要隔离 |
| 支撑域(用户中心、消息通知) | 简化版,biz + data 两层 | 逻辑相对固定,变更少 |
| 通用域(文件上传、短链) | 几乎不分层,handler 直通 DB | 纯 CRUD,分层是浪费 |
核心判断标准不是服务大小,而是业务规则的复杂度和变更频率。
敏捷开发与整洁架构的张力
矛盾在哪
- 敏捷要求快速交付可用的增量,每个 sprint 产出可部署的功能
- Clean Architecture 要求先想清楚层的边界,前期投入更多设计时间
这不是不可调和的矛盾,但需要节奏上的平衡。
实际有效的结合方式
1. 架构决策延迟但不缺席
不在项目第一天画完整的同心圆。先用简单两层(handler + repository)快速跑起来。当出现以下信号时再引入 use case 层:
- 同一段业务逻辑被两个 handler 复制粘贴了
- 需要 mock 数据库才能测试业务规则
- 产品经理开始说”在某些条件下这个流程不一样”
2. 分层是重构手段,不是初始设计
敏捷的节奏下,先让代码工作,然后在重构 sprint(或 tech debt 时间)中引入分层。这比一开始就四层架构但每层都是空壳要务实得多。
3. 用 ADR(Architecture Decision Record)记录边界决策
每次做出”这个服务要不要分 domain 层”的决策时,写一个 ADR 说明 why。后续成员不用猜为什么有的服务分了三层有的只有两层。
敏捷 + Clean Architecture 的反模式
- Sprint 1 就搭全套脚手架:四层目录结构都有了,每层只有一个空文件。这是过度设计
- 每个 story 都跨所有层改:如果加一个字段要改 domain → usecase → adapter → handler 四个地方,说明分层粒度太细或者这个服务不需要这么多层
- “重构留到以后”但从不执行:技术债积累到无法分层的程度
微服务间的 Clean Architecture
单个服务内部的分层只是一半。微服务之间的依赖关系同样适用整洁架构的思想:
┌─────────────────────┐
│ BFF / API Gateway │ ← 最外层:面向端的适配
│ ┌─────────────────┐│
│ │ 业务编排服务 ││ ← 用例层:跨服务流程编排
│ │ ┌─────────────┐ ││
│ │ │ 核心域服务 │ ││ ← 领域层:支付、交易等
│ │ └─────────────┘ ││
│ └─────────────────┘│
└─────────────────────┘
原则相同:核心域服务不依赖编排服务,编排服务不依赖 BFF。依赖方向从外向内。
违反这个原则的典型错误:支付服务为了推送通知直接调用消息服务 → 支付服务依赖了外层。正确做法:支付服务发领域事件,消息服务订阅事件。
Pitfalls
- 把框架当架构:trpc-go/Kitex 解决的是通信和治理,不是业务代码组织。用了微服务框架不等于有了整洁架构
- 微服务边界划错比内部不分层危害更大:一个服务内部写得乱但边界清晰,后续可以重构;两个服务边界耦合(共享数据库、循环调用),重构代价是拆服务
- 跨服务的 “domain 层复用”:多个服务 import 同一个 domain 包 → 实质上是分布式单体。每个服务应该有自己的领域模型,即使字段重复