返回首页
⚙️ 后端 / 架构

模块化单体完整实战 2026:微服务退潮后的后端架构回归

过去十年后端架构的标准答案是微服务,2026 年这个答案被改了:越来越多团队把拆出去的服务合回单体,同时用严格的模块边界保住微服务的组织收益。本文完整实战模块化单体:微服务的隐性成本、模块化单体与大泥球的区别、按业务能力划模块、进程内接口与依赖倒置、共享数据库的私有 schema 纪律、Outbox 模式、Strangler Fig 渐进拆分、Service Weaver 类框架的单体写分布式部署,以及什么时候才真的需要拆服务。

模块化单体 · Modular Monolith · 微服务 · 后端架构 · 领域驱动设计 · 单体优先 · Outbox · Service Weaver · 架构演进 · 渐进拆分
��

今日技术简讯

📰 技术简讯 · 2026-09-21

今日聚合 6 条热门技术内容(中文素材优先)。

🤖 AI / LLM

1. Google 发布 Gemini 3:长上下文与 Agent 能力升级

  • 链接https://blog.google/technology/google-deepmind/gemini-3
  • 来源:Google DeepMind
  • 摘要:Gemini 3 正式发布:原生多模态、长上下文窗口再扩、工具调用可靠性提升,API 定价同步下调,Agent 长流程任务的成本门槛继续下降。

2. LangGraph 1.0 GA:状态机式 Agent 编排进入稳定期

  • 链接https://blog.langchain.com/langgraph-1-0
  • 来源:LangChain
  • 摘要:LangGraph 1.0 正式 GA:以持久化状态图为核心抽象,内置检查点、人机协同中断、流式执行与回放调试,生产级 Agent 从「脚本串联」走向「可恢复的工作流引擎」。

🎨 前端 / Web

3. React 19.2 发布,React Compiler 默认开启

  • 链接https://react.dev/blog/react-19-2
  • 来源:React
  • 摘要:React 19.2 将 React Compiler 设为默认:自动记忆化消除手写 useMemo / useCallback 的心智负担,渲染性能与代码简洁度同时改善,升级路径保持兼容。

4. TanStack Start 1.0 GA:类型安全的全栈路由

  • 链接https://tanstack.com/blog/start-1-0
  • 来源:TanStack
  • 摘要:TanStack Start 1.0 正式发布:文件路由、服务端函数、流式 SSR 与端到端类型推断一体化,且不绑定特定运行时,可部署到 Node、边缘与 Serverless。

⚙️ 后端 / 架构

5. Google Service Weaver 2.0:以单体方式写、按模块拆分部署

  • 链接https://serviceweaver.dev/blog/v2.html
  • 来源:Google
  • 摘要:Service Weaver 2.0 发布:代码组织为模块化单体,框架在部署期把模块映射到独立进程或云服务,模块间调用本地即函数、远程即 RPC,「先单体、后拆分」第一次有了框架级答案。

🚀 独立开发 / OPC

6. Indie Hackers 年度调研:模块化单体成为小团队主流架构

  • 链接https://www.indiehackers.com/post/architecture-survey-2026
  • 来源:Indie Hackers
  • 摘要:Indie Hackers 2026 架构调研显示,盈利中的小型 SaaS 绝大多数以单一代码库 + 清晰模块边界运营,过早微服务被列为头号架构失误,「单体起步、边界内生长、按需抽出」成为共识路径。

数据来源:掘金 / InfoQ 中文 / 即刻 / 少数派 / HN 采集日期:2026-09-21 (UTC+8)

��

今日深度文

模块化单体完整实战 2026:微服务退潮后的后端架构回归

2014 年到 2022 年,后端架构讨论里「单体」几乎是一个贬义词,等同于落后、保守、迟早要重写。2026 年风向反转:知名公司公开分享把微服务合回单体的收益,部署延迟从分钟级降到秒级,调试从跨十几个系统追链路变成在一个进程里打断点。但这不等于「回到所有代码搅在一起的大泥球」——真正被采纳的是第三种形态:模块化单体(Modular Monolith),物理上一个部署单元,逻辑上是边界严格的模块集合。它保留单体的简单,又拿走微服务最核心的收益——清晰的边界。本文完整实战这条中间路线。


一、微服务承诺了什么,又欠下了什么

微服务的原始承诺很有吸引力:服务独立部署、独立扩缩、技术栈自由、团队与服务一一对应。这些收益在 Netflix、Amazon 那种规模下真实存在,但被行业当作默认起点照搬后,隐性成本被严重低估:

分布式的账单(每一项都在单体里不存在):
  - 网络调用:失败、超时、重试、雪崩,需要熔断器与限流
  - 数据一致性:跨服务事务变成 Saga / 补偿,复杂度数量级上升
  - 最终一致:用户下单后立刻查订单却看不到数据的诡异 bug
  - 端到端测试:要拉起十几个服务,测试环境本身就是一个工程项目
  - 调试:一个请求跨五个服务,排查要拼五条链路日志
  - 部署:版本兼容、契约测试、灰度、回滚都是平台工程
  - 组织:服务边界一旦固化,跨服务重构的协调成本极高

关键认知:微服务解决的是「规模问题」——团队规模和流量规模带来的物理约束,而不是「代码组织问题」。一个十人以下团队、单机房能扛住的业务,提前为根本不存在的规模付费,买到的只有分布式的全部代价。2026 年行业共识是:微服务的组织收益(独立演进、边界清晰)可以用模块化单体在一个进程内拿到九成,而成本只有零头。


二、模块化单体 ≠ 大泥球

「单体」有两个完全不同的物种,这也是讨论中最常见的混淆:

大泥球单体(要避免的):
  - 没有边界,任何文件都能 import 任何文件
  - 一个共享的数据库,任何代码都能 join 任何表
  - 改一个功能要理解整个系统
  - 随代码增长,测试变慢、冲突变多、没人敢重构

模块化单体(要追求的):
  - 按业务能力划分模块,模块有显式的公开接口
  - 模块内部实现对外不可见,跨模块只能走接口
  - 每个模块拥有自己的数据表,其他模块禁止直接读写
  - 单体内可独立测试每个模块,边界由架构测试强制

一句话区分:大泥球的耦合是隐式且无处不在的,模块化单体的耦合是显式且被收口到接口层的。判断标准很朴素——把订单模块整体抽成一个独立服务,需要改多少地方?如果答案是「只改接口的传输方式」,说明边界成立;如果答案是「天知道」,那它只是披着单体外衣的泥球。


三、按业务能力划分模块

模块切分的依据不是技术分层(controller / service / dao 不是模块),而是业务能力,这与领域驱动设计的限界上下文同源:

电商系统的模块划分示例:
  catalog   商品目录(商品信息、分类、上架状态)
  pricing   定价(价格、折扣规则、币种)
  inventory 库存(库存数量、锁定、扣减)
  ordering  订单(下单、订单状态机)
  payment   支付(支付单、渠道对接、退款)
  shipping  履约(发货、物流跟踪)
  identity  用户与鉴权
  billing   账单与发票

划分原则:

  1. 一个模块对一类业务规则负责:定价规则全在 pricing,库存规则全在 inventory,不要让订单模块里偷偷算价格
  2. 模块名用业务语言:能和产品经理直接对上话,而不是叫 common / manager / utils
  3. 宁可先粗后细:模块拆太细会逼出跨模块协调,先按大能力切,当一个模块内部出现清晰子边界时再分
  4. 模块数量控制在「一口气数得完」:单体阶段十几个模块足矣,几十个模块是在进程内重新发明微服务

判断边界对不对,最有效的信号是看变更是否高频地同时修改两个模块:如果是,它们大概率应该合并;如果两个模块几乎从不一起改、通信量也小,那它们就是未来真正可以拆出去的候选。


四、模块边界的代码强制:接口与依赖倒置

好的意图不等于好的架构,边界必须由代码结构和自动化测试守住。核心规则只有两条:模块只能对外暴露接口,禁止跨模块访问内部实现;依赖方向只能从外向内,通过依赖倒置反转

// modules/ordering/api.ts —— 订单模块对外只暴露这个文件
import type { PaymentGateway } from "../payment/api";
import type { InventoryService } from "../inventory/api";

/** 下单命令:跨模块入参必须是显式定义的扁平结构 */
export interface PlaceOrderCommand {
  userId: string;
  items: Array<{ sku: string; quantity: number }>;
}

/** 订单服务的公开接口,其他模块只允许依赖这个类型 */
export interface OrderService {
  placeOrder(cmd: PlaceOrderCommand): Promise<{ orderId: string; total: number }>;
}

/**
 * 订单服务工厂:依赖在边界处注入,模块内部不直接 import 其他模块的实现。
 * 依赖倒置让 ordering 只认识 payment / inventory 的接口类型,
 * 未来这两个模块被拆成远程服务时,这里只需换一个适配器实现。
 */
export function createOrderService(deps: {
  payments: PaymentGateway;
  inventory: InventoryService;
  orders: OrderRepository;        // 模块自己的仓储,表归 ordering 所有
  events: EventPublisher;        // 发事件而非直接调对方
}): OrderService {
  return {
    /** 处理下单:锁库存 → 建订单 → 发起支付,任一步失败则补偿 */
    async placeOrder(cmd: PlaceOrderCommand) {
      // 1. 先通过接口让 inventory 锁定库存(库存规则留在库存模块)
      const reserved = await deps.inventory.reserve(cmd.items);
      if (!reserved.ok) throw new Error("OUT_OF_STOCK");

      // 2. 创建本模块拥有的订单聚合,价格由 pricing 模块预先给出
      const order = Order.create(cmd.userId, reserved.lines);
      await deps.orders.save(order);

      // 3. 通过事件通知其他模块,而不是直接调用它们的内部方法
      await deps.events.publish(new OrderPlaced(order.id, order.total));
      return { orderId: order.id, total: order.total };
    },
  };
}

这段代码的要点都在「禁止」上:

  • 禁止跨模块 import 内部文件:文件系统层面把每个模块分成 api.ts(公开)与 internal/(私有),用 ESLint 依赖规则或模块可见性配置直接拒绝违规引用
  • 禁止共享领域对象:跨模块传递的是显式定义的命令 / 事件结构,而不是对方的 ORM 实体,避免对象模型把模块焊死
  • 禁止同步链式调用内部方法:模块协作优先通过领域事件,把「下单后要通知积分、通知、风控」这类扇出逻辑从订单代码里解耦出去

五、数据边界:共享数据库,私有表主权

模块化单体最有争议的实践是「允许多个模块共享一个数据库实例」。正确的纪律不是物理隔离,而是表主权

数据规则:
  - 每张表有且只有一个拥有者模块
  - 其他模块禁止直接读写这张表(代码层 + 架构测试拦截)
  - 需要别人的数据?走对方模块的公开接口
  - 跨模块的读模型用专门的查询模块或预建投影表,不临时 join

为什么不直接物理拆库?因为单进程内 ACID 事务是单体最宝贵的资产:下单流程里锁库存、建订单、写支付单可以在一个本地事务内原子完成,这是微服务要靠 Saga 和补偿机制才能勉强模拟的能力。为了想象中的未来拆分而放弃今天的强一致,是最昂贵的过早优化

同时为未来留好通道:

  • 表名带模块前缀(inventory_stockordering_orders),未来拆库时一目了然
  • 跨模块数据需求一律走接口,养成「别人的数据要请求」的习惯,这正是远程调用的心智模型
  • 用架构测试扫描 SQL 与 ORM 调用,发现 ordering 模块碰了 payment_* 表就让 CI 变红

这样,数据物理上在一起(享受事务与运维简单),逻辑上是分开的(边界纪律与未来可拆),两头的好处都占。


六、事件、Outbox 与为分布式做准备

单体里用内存事件总线就够了,但有一个关键设计要从第一天就做对:事件发布与业务写入必须原子。否则会出现「订单已落库、发布事件时进程崩溃,库存永远不释放」这类幽灵问题。Outbox(发件箱)模式是标准答案:

一次本地事务内:
  1. 写业务表(订单)
  2. 同事务写 outbox 表(待发布的事件)
事务提交后:
  3. 独立的投递器轮询 outbox,把事件发给订阅者
  4. 成功后标记已投递(失败重试,天然 at-least-once)

它在单体阶段看起来「多此一举」,但价值在演进时兑现:当某个模块未来被拆成独立服务,内存事件总线换成消息队列,业务代码一行都不用改——投递器只是从进程内调用换成了往 broker 写消息。这就是模块化单体的核心策略:用今天可以接受的小成本,把「拆分」从一场重写变成一次替换。订阅方则要从一开始就保证幂等(事件可能重复送达),这也是分布式环境下必须养成的纪律。


七、渐进式拆分:Strangler Fig

模块化单体并不承诺永远不拆。当某个模块真的触达拆分信号(下一节详述),正确姿势是绞杀者模式(Strangler Fig)渐进剥离,而不是大爆炸重写:

拆分一个模块的标准流程:
  1. 确认边界干净:被拆模块的所有外部依赖都已经走接口(前置条件)
  2. 在接口层引入传输抽象:本地实现 / 远程实现可切换
  3. 数据先合后分:先在原库内把该模块的表物理隔离或只读化
  4. 部署新服务,流量按比例灰度(先内部账号,再 1%,再全量)
  5. 双写或事件同步保证迁移期数据一致,逐表切换所有权
  6. 旧模块代码从单体删除,绞杀完成

整个过程中系统始终可发布、可回滚。每一步都小到一天内可以完成或撤回。拆分失败的项目几乎都是想一步到位:停服迁移、一次性切库切流量,出了问题无法定位在哪一步。模块化单体把拆分变成一系列低风险的机械操作,这是它相对微服务最大的期权价值——你保留了拆分的自由,但把决策推迟到信息最充分的时候。

Google Service Weaver 2.0 代表的框架路线把这个思想推到了极致:代码始终以模块化单体书写,部署时由框架决定每个模块跑在同进程还是独立进程,模块间调用在本地是普通函数、被拆开时自动变成 RPC。开发者只声明边界,不预先支付分布式的开发成本,部署拓扑变成一个配置项而非架构决策。


八、部署、仓库与工程纪律

模块化单体的工程配套比微服务简单一个量级:

事项 模块化单体做法
代码仓库 单仓多模块(monorepo 结构),无需跨仓版本协调
CI 一次构建产出一个制品;按变更模块做增量测试
测试 模块内单元测试快而多;少量跨模块契约测试与端到端测试
部署 一个制品、一条流水线、秒级发布;回滚就是切回上一个制品
环境 一套依赖(一个数据库),本地 docker compose 一条命令起全栈
可观测 单进程内链路天然完整;结构化日志带模块名即可定位

需要守住的纪律:架构测试必须进 CI。用依赖规则断言「inventory 不允许 import ordering/internal」、用数据访问扫描断言「只有 owning 模块能碰对应表」。边界这种东西,靠 code review 的自觉会在三个月内腐烂,只有自动化红线能长期维持。这是模块化单体唯一需要认真投入的「基础设施」。


九、什么时候才真的需要拆出去

模块化单体不是反微服务,而是反对「过早」。以下信号出现时,拆分才是理性决策:

  1. 独立扩缩:某个模块的负载特征与其他模块完全不同(视频转码吃 CPU、报表跑 OLAP),必须用不同机器形态
  2. 故障隔离:某个模块的崩溃会拖垮全进程,且它确实不稳定(早期的第三方渠道对接是典型)
  3. 独立发布节奏冲突:某模块一天发十次,每次全量回归拖累所有人
  4. 团队规模:多个十人小组在同一个代码库上频繁冲突,协调成本超过分布式成本
  5. 技术约束:某模块需要主语言生态之外的能力(特定 ML 运行时、专有系统 SDK)
  6. 数据 / 合规边界:法规要求某类数据物理隔离或独立审计

注意这些信号全部是组织与运维层面的物理约束,不是代码美观问题。在这些信号出现之前,单体的简单性本身就是巨大的竞争优势——小团队的吞吐量优势往往来自没有协调成本,而这正是大公司羡慕不来的东西。


十、避坑指南

  1. 不要用技术分层当模块:controllers / services / repositories 是代码组织,不是边界;模块必须按业务能力切
  2. 不要留 common 大杂烩:公共模块会慢慢吞噬一切,真正共享的东西按用途拆成小内核(money、time、ids),且只放无业务规则的纯工具
  3. 不要让 ORM 导航属性跨模块:延迟加载的关联查询会在你看不见的地方越过表主权边界,跨模块引用一律显式接口
  4. 不要在单体内搞 HTTP 自调用:模块之间用进程内接口,自己调自己的 localhost 是没有任何收益的伪分布式
  5. 不要过早共享数据库的「便利」:一次跨模块 join 省下的十分钟,会在拆分时用几周偿还
  6. 不要忘记事件幂等与契约测试:这些是未来拆分的预置轨道,临时补课代价极高
  7. 不要把「将来会拆」挂在嘴边当借口:好架构的标准是今天舒服且明天可演进,而不是为假想的明天牺牲今天的一切

十一、结语

架构的本质是在「当前的简单」与「未来的变化」之间下注。微服务是在变化尚不确定时就预付了全部分布式成本,大泥球是彻底放弃对变化的准备,而模块化单体走的是中间路线:用严格的模块边界购买未来的期权,用单进程部署享受今天的简单,并在信息充分时以绞杀者模式渐进行权

对绝大多数中小团队和独立开发者,这是 2026 年风险调整后收益最高的后端架构。务实建议:新项目从模块化单体起步,第一天就立好三条规矩——跨模块只走接口、每张表有唯一属主、事件用 Outbox 原子落库,并用 CI 强制它们。把「拆不拆、什么时候拆」留给真实的负载与组织信号去回答。好的架构不是为终极形态而建,而是让每一次演进都不必推倒重来。


参考资料

�� 同主题文章

⚙️ 后端 / 架构 分类更多