单体优先:九个模块、一条依赖方向与一次边界检查

九个模块、五万四千四百行业务代码,一个人维护的单体为什么不该急着拆服务:一条从 ui 到 infra 的四层依赖方向、一次八秒跑完的边界检查、一条三选三的模块独立判据,把跨模块改动从六小时压到一点五小时,再按季度复算收益。

拆出去的每个服务,都要用你仅剩的周末来养。

一、朋友建议拆成三个服务的那一周

2026 年 3 月,产品写到第 10 个月,代码 62,400 行。一位朋友发来长邮件,建议把 analytics(6,800 行)和 search(5,200 行)拆成两个独立服务,理由是「以后好扩容」。陆远算了一个晚上:每多一个服务,就多 1 套部署、1 条链路、1 份备份,按月折合约 2 小时;3 个服务就是 6 小时/月。同一周,他在 analytics 里翻出自己三个月前写下的一行 import,直接引用了 billing 的订单模型。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

那行 import 没有报错,也没有人拦他,因为仓库里从来没有一份写下来的依赖方向。它安静地待了三个月,直到一次改订单字段时才炸出来:analytics 的报表跟着挂了两天。

如果当时真拆成三个服务,这行 import 会变成一次跨服务调用,故障面从「一个报表页面」扩大到「两个服务之间的接口契约」;而他要付的部署、链路与备份成本,一分钟都不会少。

一个人维护的系统,服务数量是成本项,不是能力证明。 每拆一个服务,你买到的是独立扩容,付出的是多一套部署、多一条链路、多一份备份,以及半夜多一个要查的地方。

单体的真问题不是耦合,是没有任何人声明过依赖方向。 耦合是结果,方向缺失是原因;先补方向,再判断要不要拆,顺序反了就会把问题搬进更贵的地方。

边界检查要能在 8 秒内跑完,否则它不会留在 CI 里。 超过半分钟的检查,第一次失败时就会被注释掉。

二、九个模块:从 12,800 行到 1,600 行

业务代码 54,400 行,分成九个模块。行数不是评价标准,但它能告诉你改动会往哪里集中。

模块行数职责
projects12,800核心业务对象与权限入口
files9,600上传、转码、签名 URL
shared8,400基础类型、工具、错误码
analytics6,800事件聚合与报表
search5,200索引与查询
billing4,200订阅、订单、退款
notifications3,400站内信与邮件模板
admin2,400内部后台
auth1,600登录、会话、密钥

九个模块平均 6,044 行;最大 12,800 行,占 23.5%;最小 1,600 行,占 2.9%。只按行数看,最大的 projects 距离「单一模块不超过 30%」的红线还有 3,520 行空间(54,400 × 30% = 16,320)。

但 projects 的问题不在行数,在位置。它既是业务对象,又是权限入口,近 90 天 22 次跨模块改动里,多数从它出发或经过它。这意味着拆分它的收益最高,风险也最高——所有模块都在引用它。

把这组改动摊开:22 次跨模块 × 6 小时 + 16 次单模块 × 1.5 小时 = 156 小时/季度。跨模块改动占 57.9% 的次数,却吃掉 84.6% 的改动时间(132 ÷ 156)。

shared 8,400 行排在第三,是最容易被当成垃圾桶的模块。它现在只有基础类型、工具与错误码;一旦有人把「反正大家都要用」的业务规则放进去,它就会长成第二套 domain,四层方向随之失效。

最小的两个是 admin 2,400 行与 auth 1,600 行,加起来只占 7.4%(4,000 ÷ 54,400)。它们不需要边界讨论,需要的是别被别的模块顺手改掉。

三、一条依赖方向:ui → app → domain → infra

四层,箭头只允许向左。逆向引用当前是 0 条——这是清完那行 import 之后的状态,不是天生的。

层允许依赖禁止依赖放什么
uiappdomain、infra页面、表单、路由
appdomainui、infra 的实现用例编排、事务边界
domain自己定义的接口ui、app实体、规则、错误码
infra无(只被依赖)ui、app、domain 的实现数据库、外部服务客户端

规则只有三条:箭头只向左;逆向引用保持 0 条;跨层(隔层)调用必须经过 shared 暴露的类型与接口,不允许直接 import 实现。shared 是唯一允许被四层同时引用的模块,所以它必须只放类型、工具和错误码,不能放业务判断。

边界检查脚本 120 行,跑一次 8 秒,做两件事:扫描 import 路径,输出逆向引用清单;统计每个模块被哪些模块引用。它不检查命名,也不检查代码风格——那些交给别的工具,混在一起会让脚本变慢,也就留不住。

120 行里大约 70 行在读目录、20 行解析 import、30 行输出报告。它不做语法树分析,只做路径匹配——够用,也够快。

逆向引用能清到 0,靠的不是自律,是脚本进了 CI 之后每次 push 都拦。第一次跑报了 11 条,清完用了两个下午,其中一条就是那行 billing 的 import。

一个模块要不要单独拆出去,看三条同时成立:

  • 被 3 个以上模块引用;
  • 有自己的数据表;
  • 有自己的错误码。

三条缺一条就先留在原地。只有两条的模块拆出去,会得到一个既没有独立数据、又要别人替它定义错误的服务,维护成本立刻翻倍。

四、复算:为什么三个服务等于 72 小时

54,400 ÷ 9 = 6,044.4 行
12,800 ÷ 54,400 = 23.5%
1,600 ÷ 54,400 = 2.9%
每服务维护 2 小时/月 × 3 个服务 × 12 个月 = 72 小时/年
边界检查 8 秒 × 16 次/月 × 12 个月 = 1,536 秒
1,536 ÷ 60 = 25.6 分钟/年
8 次 × (6 − 1.5) 小时 = 36 小时/季度
36 × 4 = 144 小时/年
144 × 60 ÷ 25.6 = 337.5

72 小时是拆三个服务的年成本,144 小时是边界清楚之后的年收益。两者不在同一栏,但可以放在同一张桌上:不拆,省下 72 小时;把边界补上,再省 144 小时。两笔加起来,是 216 小时的差距。

72 小时看起来不多,只占 2,080 小时的 3.5%,但它是每年都要付的,而且随服务数量线性增长:5 个服务就是 120 小时/年。真正的问题不是这 72 小时,是它换来的能力你未必用得上。

16 次/月来自 8 次发布,每次 push 与 release 各跑一次。25.6 分钟/年换来 144 小时/年,比值 337.5;这个数字不必精确,它的作用是说明边界检查是整套方法里性价比最高的一项。

如果只在 release 时跑,次数减半、时间减半,但 push 阶段就发现不了逆向引用——而 push 之后、发布之前,正是修复最便宜的时机。

144 小时的前提是那 8 次改动真的降为单模块 1.5 小时。如果只有 3 次降级,年收益变成 3 × 4.5 × 4 = 54 小时,仍远超检查成本,但说明边界只解决了一部分问题,剩下的该去看 06 的抽象时机与 07 的重构判据。

五、行动清单与复算验收

  • 把九个模块的行数抄成一张表,标出最大与最小两个。
  • 在仓库根写一份依赖方向说明,只写四层与箭头方向,不写例外。
  • 给 shared 加一条规则:只放类型、工具和错误码,出现业务判断就移回 domain。
  • 写一个 120 行的边界检查脚本,扫描 import 路径并输出逆向引用清单。
  • 把脚本挂进 CI,push 与 release 各跑一次,单次预算 8 秒。
  • 本月内清掉脚本报出的全部逆向引用,让清单归零。
  • 给每个候选拆分模块核三条:被 ≥3 个模块引用、有自己的数据表、有自己的错误码。
  • 季度末统计跨模块改动次数,与 22 次对比,记录降为单模块的是哪几次。

验收标准:边界检查单次 ≤ 8 秒、逆向引用 0 条、最大模块占比 < 30%,且下个季度跨模块改动不超过 14 次(38 次里的 36.8%)。

最后一条不是拍脑袋:22 次里预计 8 次可以降级,剩 14 次;14 ÷ 38 = 36.8%。如果实际只降到 18 次,说明边界画得不够细,先别急着加规则,去看那 4 次为什么仍然跨模块。

为什么是 16 次而不是 8 次:8 次发布对应 8 次 release 检查,每次发布前至少还有一次 push 触发的检查,合起来 16 次;多出来的 8 次是提前拦截,也是最便宜的那一半。

使用边界与实操验收

模块少于五个时不要画四层。 三个模块的系统画四层只会产生空层,空层会逼着人把代码往上塞;先按实际调用画两层,等模块数上来再补。

依赖方向不能替代接口设计。 方向对了,但接口把内部结构暴露出去,改动仍会穿透边界;脚本只能查 import,查不了参数里塞了谁的实体。

不是所有跨模块改动都该降级。 计费影响通知这类改动本质就是跨模块,强行合并会把两个模块焊死;要降的是那些「因为找不到入口才绕过去」的改动。

这套表不适用于已经拆开的服务。 服务间的链路、备份、部署成本已经付出,再用单体表评估只会得出「当初不该拆」的结论,对现在没有帮助。

什么时候该停下来。 如果连续两个季度跨模块改动都低于 10 次,边界已经够清楚,不要再加规则;继续加规则,维护成本只会从代码转移到规则本身。

关键参考与核验范围

核验日期:2026-10-26。以下文档用于核对概念与规则,本文所有数字均取自全组教学基线,不取自这些文档。

引用口径。 四份文档只用于确认依赖声明、决策记录、技术债与重构手法的原始概念;模块行数、服务维护小时与降级次数均来自本文的教学基线,不来自文档。

核验范围。 上述四个入口在 2026-10-26 实测可公开访问(HTTP 200);未核验各项目的最新版本、价格与配额。

如何自行复核。 用你自己的模块表重算最大值与最小值占比,再数一遍最近 90 天的跨模块改动;把这两个数代入「8 次 ×(6 − 1.5)× 4」的同一算式,看收益是否仍大于你拆服务要付的维护小时。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元