本篇要搞懂的 4 件事:微服务设计的真正重点不是"拆成几个"而是"演进式架构"、什么是"小单体微服务"这个坑、微服务里到底有几种边界(逻辑/物理/代码)各管什么、清晰的边界怎么让"拆分和重组"变得轻松。
⚠️ 这一讲比较"虚"——讲的是边界这种看不见摸不着的东西。别急着记名词,抓住一句话就行:好的边界,是为了让你以后改架构时不痛苦。
承接前文:前面几讲学了用事件风暴划聚合、划限界上下文、定领域模型。15 讲把这些落到一个词上——边界。微服务里其实藏着好几层边界(聚合之间的逻辑边界、微服务之间的物理边界、代码目录之间的代码边界),这一讲就讲清楚:这些边界分别在架构演进里起什么作用,以及为什么只画了物理边界(拆软件包)的微服务,本质上还是个单体。
事件风暴 → 划出聚合、限界上下文、领域模型
↓ 落到"边界"这个词
15:微服务有三种边界
逻辑边界(聚合之间)→ 决定怎么拆怎么重组
物理边界(微服务之间)→ 决定部署运行隔离
代码边界(代码目录之间)→ 决定代码重组的影响范围
↓ 边界清晰
架构能轻松演进,而不是"一夜回到解放前"
微服务的重点:演进式架构,不是"拆成几个"
很多人一上来就纠结:单体要拆成多少个微服务?作者直接说——这不是重点。
Martin Fowler 提微服务时强调的核心特征是演进式架构(Evolutionary Architecture)。
🪞 辅助理解(演进式架构是啥):把它想成"乐高式建筑"。演进式架构 = 以增量的、非破坏性的变更为第一原则。你想加一层楼、想把厨房挪到隔壁,不用把整栋楼推倒重盖,拆几块积木重新拼就行。
那怎么判断你的微服务设计得好不好?就看一条标准:
💡 随着业务变化,你需要不断地拆分、重组微服务。如果这个过程很轻松、不会大幅增加开发和维护成本,那就设计对了;如果每次重组都伤筋动骨、推倒重来,那就是设计错了。
用 DDD 设计的微服务,靠限界上下文 + 聚合实现了内外解耦,所以业务功能能像积木一样模块化地重组更新——这正是支撑架构演进的底气。
“微服务"还是"小单体”?——最常见的坑
有的团队拆单体时,不建领域模型,只是把原来一个大软件包,按业务功能切成几个小软件包,就叫"微服务"了。但每个小包里头,代码还是老一套的集中式三层架构:高度耦合、逻辑边界不清。作者给它起了个名字——小单体微服务。
看图抓什么:左边是同一块大石头(= 原来的大单体)。
- 上排(❌):石头被磨成了三个光滑的铁球。看着是"拆开了、变小了",但每个球内部是实心一坨、没有结构。这就是小单体微服务——只是把大坨切成了小坨。
- 下排(✓):石头被做成了三堆彩色积木块。每块颜色分明、边界清晰,可以随时拆下来、换地方、重新拼。这才是有内部边界的微服务。
💡 关键差别不在"大小",在"内部有没有清晰的边界结构"。光滑铁球虽小,也搬不动里头的零件;积木虽是一堆,却能任意重组。
小单体的下场:随着需求增加,这些小铁球会慢慢膨胀。等到哪天你想把其中一部分功能拆出去、或和别的服务重组,你会发现——它们早已悄悄长成了臃肿的大单体,内部依旧高耦合、边界不清。于是你只能一遍遍重复"大单体 → 拆微服务"的痛苦过程。作者吐槽得很到位:“辛辛苦苦好多年,一夜回到解放前。”
🪞 辅助理解:这就像你写一个脚本/小工具,所有逻辑(读输入、处理、输出)全塞进一个 800 行的
main()。它能跑,但你想把"处理"那段抽出来复用到另一个工具时,根本扯不开——函数之间互相调来调去缠成一团。"拆成多个文件"不等于解耦,关键是模块内部有没有清晰的职责边界。
问题的本质就是边界:小单体只定义了一个维度的边界——微服务之间的物理边界(拆成了不同的包/进程)。但它漏掉了微服务内部该有的逻辑边界和代码边界。少了这两层,它本质上还是单体。
微服务的三种边界
回顾一下 DDD 的设计链路,边界是一层层长出来的:
事件风暴 → 梳理出实体等领域对象
→ 业务紧密相关的实体组合成【聚合】 ← 聚合之间是第一层边界
→ 一个或多个聚合圈进一个【限界上下文】 ← 限界上下文之间是第二层边界
→ 形成领域模型
为了好理解,作者把边界归成三类:
| 边界 | 在哪 | 管什么 | 是虚是实 |
|---|---|---|---|
| 逻辑边界 | 微服务内,聚合与聚合之间 | 界定业务高内聚的对象聚类;不同聚合的代码隔离在不同目录 | 虚拟边界,但可随时"实体化"成物理边界 |
| 物理边界 | 微服务之间 | 部署和运行的隔离——不同进程、不同环境、互相物理隔离 | 真实存在(进程级隔离) |
| 代码边界 | 微服务内,不同职能/聚合的代码目录之间 | 隔离不同功能的代码,控制重组的影响范围 | 目录级隔离,直接体现业务边界 |
🪞 辅助理解(三种边界打个比方):把微服务想成一栋联排别墅小区。
- 物理边界 = 每栋别墅之间的院墙(独立门牌、独立水电,互不干扰,对应独立进程独立部署)。
- 逻辑边界 = 一栋别墅内部的房间隔断(客厅、厨房、卧室功能分明,虽然在同一栋楼里,但职责清楚——对应聚合之间的边界)。
- 代码边界 = 你给每个房间贴的标签 / 收纳分区(东西分门别类放好,搬家时一箱一箱搬,不会乱——对应代码目录的隔离)。
小单体微服务的毛病就是:只砌了院墙(物理边界),别墅内部是个没有隔断的大开间。
逻辑边界:架构演进的"规则"
作者特别强调:逻辑边界在架构演进中意义最大。
💡 微服务的演进不能随心所欲,它得按聚合这个单位来。架构演进时:业务端以聚合为单位重组业务能力,代码端以聚合的代码目录为单位重组代码。因为 DDD 设计的聚合天生高内聚、聚合之间松耦合,所以拆和重组都很省力。
来看一个真实例子——一个微服务里装了两个聚合:
看图抓什么:
- 左边是一个微服务(
leave请假服务)的代码目录树。domain层下面装了两个聚合:leave(请假,含 Applicant、Approver、Leave、LeaveType 等实体)和person(人员,含 Leader、Person、Relationship 等实体)。每个聚合内部结构一模一样——都有entity(实体)、event(事件)、repository(仓储)、service(领域服务)。 - 两个聚合各占一个独立的代码目录(红框框出来了),它们的代码是物理隔离的——这就是代码边界,而它们在业务上的隔离就是逻辑边界。
- 右边两个六边形:红色箭头表示,因为边界清晰,可以直接把某一个聚合(连同它的整个代码目录)原封不动抽出来,独立成一个新的微服务。比如
person聚合扛不住高并发了,就把它单独拆出去。
💡 拆分之所以轻松,是因为逻辑边界 = 代码边界 = 可拆分的最小单位。聚合代码本来就归拢在一个目录里、对外松耦合,拆的时候"整箱搬走"即可,不用从一坨代码里一行行抠。
反过来也成立:可以把多个微服务里功能相似的聚合抽出来、重组成新的通用微服务——作者点了一句:“现在你是不是有点做中台的感觉了?”(通用能力沉淀 = 中台)。
物理边界 & 代码边界:一句话各自的活
- 物理边界:站在部署/运行视角看微服务之间的隔离。不同微服务跑在不同进程、不同环境里,关注的是服务调用、容错、运行隔离。
- 代码边界:用于微服务内部不同职能代码的隔离。因为领域模型和代码模型是映射的,所以代码边界直接反映业务边界。它的价值是控制代码重组的影响范围——改一块不会牵连一片,重组时以聚合代码为单位即可。
正确理解边界:聚合一定要拆成微服务吗?
逻辑边界和代码边界让演进省力,但有个常见误区——聚合是不是必须做成微服务?(即逻辑边界一定要等于物理边界吗?)
💡 答案:不一定。 聚合是"可以拆成微服务的最小单位",注意是"可以",不是"必须"。逻辑边界(虚)可以按需升级成物理边界(实),但没必要一上来就全拆。
为什么不要一拆到底?因为有过度拆分这个坑:
| 拆太细的代价 | 说明 |
|---|---|
| 集成成本 ↑ | 服务越多,服务间调用、联调越麻烦 |
| 发布成本 ↑ | 一个需求要协调发布好几个服务 |
| 运维成本 ↑ | 部署、扩缩容、配置管理对象翻倍 |
| 监控定位成本 ↑ | 一个请求跨多个服务,排查问题像查链路追踪 |
🪞 辅助理解(拆分的节奏):拆微服务像搬家分箱。你能力还不强、管理工具不全时,分太多箱反而找不到东西(过度拆分)。先粗粒度地拆,但箱子内部要分区整齐(逻辑边界、代码边界清晰)。等你管理能力上来了,随时能把某个分区单独打包搬走(按聚合拆出新微服务)。前提是内部边界一直保持清晰。
作者最后补一句铁律:微服务内聚合之间的服务调用和数据依赖,必须符合高内聚、松耦合——否则就算边界画得再好看,拆的时候照样扯不动。
总结
这一讲就讲一个字:边界。微服务里有三种边界,各司其职:
- 逻辑边界——微服务内聚合之间的边界。虚拟的,强调业务高内聚,可按需升级成物理边界(聚合独立成微服务)。它是架构演进的规则。
- 物理边界——微服务之间的边界。强调部署运行的隔离,关注服务调用、容错、运行。
- 代码边界——不同层 / 聚合之间代码目录的边界。强调代码隔离,方便演进时按聚合整块重组。
三者配合,才能做到"业务能力高内聚、代码松耦合",从而轻松实现微服务的拆分与组合,支撑长期演进。最大的反面教材是小单体微服务——只砌了物理边界(拆软件包),内部没有逻辑/代码边界,本质还是单体,迟早膨胀成大单体,“一夜回到解放前”。
⚠️ 全篇最该记住的一句:边界清晰的微服务,演进是"重组积木";边界不清的微服务,演进是"推倒重盖"。
一句话速记
微服务的命门在边界——逻辑边界定怎么拆(按聚合)、物理边界管运行隔离、代码边界控影响范围;三层边界齐全才叫微服务,只拆软件包那是会膨胀的"小单体",迟早回到解放前。
思考题
你平时让 AI 帮你写工具/脚本时,其实也在做一模一样的"边界"决策——
- 检验你的"边界感":你让 AI 写的某个小工具 / 脚本,是一坨大
main()(光滑铁球),还是模块化、每个文件职责单一可单独复用(彩色积木)?拿一个你最近的项目对照上面那张"铁球 vs 积木"图判断一下。 - 设计先于代码:既然你"架构会设计、代码靠 AI",那正好——边界是你该亲自定的,代码可以交给 AI。下次写工具前,先用一句话给 AI 划好"逻辑边界":哪几个模块、各自管什么、谁不许调谁。看看产出的代码是不是真的更好拆、更好改。
- 过度拆分的反思:你有没有为了"显得专业"而把一个小脚本拆成七八个文件,结果改一个功能要翻五个文件?回看上面"过度拆分代价"那张表,想想你的拆分粒度配不配得上当前的维护能力。
延伸:怎么让 AI 写代码时遵守边界?
边界画得再好,AI 一边写一边越界也白搭。核心认知一句话:
AI 是概率性的,边界必须是确定性的。 靠"叮嘱 AI 遵守边界"永远不可靠——必须是 告诉它 → 机器自动查 → 违规弹回来重写 这个闭环。prompt 写得再好也只是"预防",真正锁死边界的是那个确定性的门(架构测试工具,如 Python 的 import-linter / Java 的 ArchUnit)。
按这个闭环分三层落地:
第一层 · 告诉它(预防):把"边界铁律"写进 AI 指令文件
在项目的 AGENTS.md / CLAUDE.md 里,用命令式大白话直接列出依赖规则(别让规则只躺在配置文件里——AI 不会把 pyproject.toml 当指令读)。示例:
## 边界铁律(写任何代码前必读)
1. domain/ 不许 import 任何外层(api/application/infrastructure)和任何框架。
2. 跨模块禁止 import 对方 domain/ 或内部实现;只能:发领域事件 或 调对方 application 接口。
3. 仓储:接口在 domain/,实现在 infrastructure/。
4. 业务规则只能落在 domain/(实体/领域服务);application 只做编排,不写规则。
5. 写完必须自检:跑一遍架构测试(import-linter),不过不算完成。
💡 第 5 条最关键——它把"自查"写成了 AI 的交付定义,而不是可选项。
第二层 · 当场自查(检测):让检查发生在 AI 的回合内
最大的杠杆点。别等到 git commit 才跑边界检查——那反馈链太长。把它提到"AI 每改完文件就自动跑":
- 简单版:在指令里要求 AI 写完自己跑架构测试,把报错贴回来继续修。
- 彻底版(推荐):加一个编辑器/Agent 的 PostToolUse / Stop hook,在 Edit/Write 之后自动跑 import-linter,违规信息直接回灌给 AI——这样它在同一个 session 内就自纠,根本到不了 commit。
🪞 与其等考完试再判卷(commit 时),不如每写一题旁边就有人立刻说"这题越界了"(edit 时)——AI 当场就改对了。
第三层 · 强制兜底(执法):pre-commit / CI 真的拦得住
- 本地
pre-commit install(没装的话钩子等于摆设,能直接 commit 绕过)。 - CI 上跑同一套检查——本地绕过了,合并前还有一道闸。
这是"AI 和你都翻车时的最后一道墙",确定性最强,必须在。
两个提效小招
- 给 AI 一个模板模块:让它"照着
modules/已有模块/的目录和分层新建",照抄结构比"按描述生成结构"准得多。 - 先报位置再动手:复杂功能先让 AI 说清"每个文件放哪层、跨没跨模块",你确认后再让它写——在写之前就掐掉大部分越界。
💡 一句话收口:边界是你(会设计架构的人)该亲自定的,代码可以交给 AI;但定完边界一定要配一个机器执法的门,否则 AI 的"遵守"只是概率事件。


