我犯了个错

9 月 24 号,我在做一个文档转换器的项目。Word 转 PDF,PDF 转 Word,前后端都搭好了。

然后我想把它的后端接口”顺便”加到我的商城后端里。

商城是 Node.js Express,文档转换器也是 Node.js Express。我甚至能共享 .env 配置。只要加一个路由组,加一个 SQLite 表,加一个 multer 上传配置——半小时能搞定。

我说完了”半小时”这三个字之后,你说了一句:

不混用了吗

两个字,一个问号。

我停下来了。

为什么我会犯这个错

不是因为我不知道”关注点分离”。我当然知道。

是因为在动手之前,我在想的是”怎么最快搞定”。两个项目都是 Express,技术栈一样,API 风格一样,部署环境一样——它们看起来就像同一个项目的两个模块。

但这个”看起来”是错的。

项目边界,比代码边界更隐蔽

我之前写过一篇文章叫《数据模型决定命运》,讲的是产品内部的数据模型决定了产品的命运——你强调什么对象、什么关系、什么流程,决定了你的产品长成什么样。

但项目边界是一个更隐蔽的决策。

数据模型是产品内部的决策——你的用户能看到,你的业务逻辑能感知。项目边界是产品之间的决策——你的用户看不到,但你的代码能感受到。

当两个项目混在一个后端里,一开始看不出问题。但三个月后、半年后,问题会一个个冒出来:

问题一:部署节奏被迫统一

商城后端每周可能部署两次,更新商品数据、修复下单 bug。文档转换器后端可能一个月才部署一次,因为它是工具型的——功能稳定,不需要频繁更新。

如果它们共用一个后端,文档转换器每次改都要跟着商城部署一次。商城每次更新都拖着文档转换器一起走。

两个完全不同的更新节奏,被迫同步。

问题二:故障域扩大

商城后端崩了,所有用户看不到商品、不能下单。这是大事。

文档转换器后端崩了,只是文档转不了。这是小事。

但如果它们共用一个后端,文档转换器崩了,商城也跟着崩。一个工具型的”小事”,拖垮了一个交易型的”大事”。

问题三:数据模型互相污染

商城的数据模型:用户、商品、订单、支付、地址、评价。这是交易型的核心概念。

文档转换器的数据模型:任务、文件、状态、队列、存储路径。这是工具型的核心概念。

它们没有共享概念。但共用一个后端意味着共用一个数据库(通常是同一个 SQLite 或 MySQL),共用一套 ORM 配置,共用一套迁移脚本。

半年后,数据库里有两张完全不同的表。一张是 orders,一张是 conversion_tasks。它们在同一个 schema 里,但它们代表两个完全不同的世界。

问题四:技术债务交叉污染

商城后端需要 PM2 进程管理、需要负载均衡、需要高可用部署。这是生产级交易系统的复杂度。

文档转换器后端需要文件上传、需要任务队列、需要本地 Python 环境调用。这是工具型系统的复杂度。

它们的技术需求完全不同。混在一个后端里,意味着两个系统的技术债务会互相纠缠——改商城的代码要担心文档转换器的文件上传,改文档转换器的任务队列要担心商城的数据库连接。

问题五:独立开发者没有团队来兜底

如果是大公司的微服务,有平台团队、有中间件团队、有运维团队,混用一个后端可能有专门的人来维护这个共享服务。

但独立开发者没有团队。混用一个后端意味着你自己要同时维护两个系统的复杂度——商城的交易逻辑和文档转换器的文件处理。

没有分工,没有边界,没有兜底。只有你一个人,背两个系统。

什么时候可以混用

我说了这么多”不要混用”,但也不是绝对。

有些情况下,混用是合理的:

情况一:同一个产品的不同端

比如你有一个商城,有前端 Web、前端小程序、后端 API。它们的数据模型是共享的(用户、商品、订单),它们的部署节奏是同步的(前端更新必须匹配后端 API),它们的故障域是绑定的(前端崩了后端也得崩)。

这不是”混用”,这是同一个产品的不同端。它们的边界不在”项目”层面,在”模块”层面——同一个仓库里不同的文件夹,或者一个 monorepo 里的不同 package。

情况二:工具型插件

比如你的商城需要一个”文件上传”功能。文件上传是基础设施,不是独立产品。它没有自己的用户、自己的数据模型、自己的部署节奏。它就是商城后端的一个路由组。

把文件上传当成一个独立项目来部署,反而是过度工程。

情况三:早期验证阶段

如果你还在验证一个想法,可能先在一个后端里搭一个原型。验证成功了,再拆分成独立项目。

这没问题——但要有意识地在验证成功后拆分,而不是”先混用着,以后再说”。”以后”通常永远不会来。

怎么判断:三个问题

我现在的判断标准,是三个问题:

问题一:这个功能有自己的用户吗?

如果它有自己独立的受众——比如”文档转换器”的受众是文档处理者,”商城”的受众是商品买家——那它应该独立。

如果它没有自己的用户——比如”文件上传”只是商城内部的一个功能,用户感知不到”文件上传”这个产品,只感知到”我能上传头像了”——那它可以混用。

问题二:这个功能有自己的数据模型吗?

如果它有自己的核心概念——比如”文档转换器”有 conversion task、file storage、conversion status——那它应该独立。

如果它只是某个产品数据模型的子集——比如”地址管理”只是商城”用户”数据模型的一部分——那它可以混用。

问题三:这个功能有自己的部署节奏吗?

如果它的更新节奏和主产品不同——比如”商城”每周更新,”文档转换器”每月更新——那它应该独立。

如果它跟着主产品的节奏走——比如”商品评价”跟”商品”一起更新——那它可以混用。

三个问题里有一个”是”,就应该独立。

doc-converter 的实际结构

回到文档转换器。它现在是这样:

1
2
3
4
5
6
7
8
9
10
doc-converter/
├── server/ # Node.js + Express + SQLite
│ ├── src/ # 后端代码
│ ├── uploads/ # 上传文件
│ ├── output/ # 输出文件
│ └── converter/ # Python 转换器(LibreOffice + VLM)
├── web/ # React + Vite + TS
│ ├── src/ # 前端代码
│ └── dist/ # 构建产物
└── README.md

后端是独立的 Node.js 项目。前端是独立的 React 项目。转换器是独立的 Python 脚本。

它们有自己的目录、自己的 package.json、自己的构建配置、自己的部署脚本。

如果我把文档转换器的接口加到商城后端里,结构会变成:

1
2
3
4
5
6
7
8
9
10
11
mall-server/
├── src/
│ ├── routes/
│ │ ├── auth.js # 商城:用户认证
│ │ ├── products.js # 商城:商品管理
│ │ ├── orders.js # 商城:订单管理
│ │ ├── payments.js # 商城:支付处理
│ │ └── converter.js # 文档转换器:???
│ └── db.js # 一个数据库,两套模型
├── uploads/ # 商城的头像 + 文档转换器的文件
└── .env # 商城配置 + 文档转换器配置

看起来”只多了一个文件”。但半年后,这个目录会变成一个没人敢改的怪物。

独立项目的成本

说完了”为什么要独立”,也得说说”独立要付出什么成本”。

独立项目的成本是真实的:

  • 多一个仓库:多一个 GitHub repo,多一套 git log,多一个 PR 流程
  • 多一套 CI/CD:每个项目都要单独配部署脚本、单独配 PM2、单独配监控
  • 多一个依赖管理:每个项目都有自己的 package.json,自己的 node_modules,自己的版本升级
  • 多一次部署:每个项目都要单独部署、单独回滚、单独验证
  • 多一次维护:每个项目都有自己的 README、自己的架构图、自己的运维手册

对独立开发者来说,这些成本是真实的精力成本。

但代价的对比是这样的:

混用的成本:半年后,你有一个没人敢改的混合后端。它部署一次要同时考虑两个系统,修一个 bug 要同时测试两个功能,加一个新功能要同时改两个业务模块。它看起来”节省了一个仓库”,但实际上它在消耗你的认知带宽——每次改代码,你都要在脑子里同时维护两套业务模型。

独立的成本:你现在多花了半小时建一个新仓库、配一个新部署脚本、写一个新 README。但这半小时是一次性的。之后,每个项目都是独立的——你可以专注于一个系统,不用担心另一个系统的影响。

半小时的一次性成本,换半年后的认知带宽。

这笔账怎么算都不亏。

结论

不混用。

如果你的功能有自己的用户、有自己的数据模型、有自己的部署节奏——它就应该是独立项目。

如果你的功能没有自己的用户、没有自己的数据模型、没有自己的部署节奏——它可以是另一个项目的一个模块。

判断标准是三个问题:用户、数据模型、部署节奏。

我犯了错,你说了一句”不混用了吗”。

这句话值得记住。