我犯了个错
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 | doc-converter/ |
后端是独立的 Node.js 项目。前端是独立的 React 项目。转换器是独立的 Python 脚本。
它们有自己的目录、自己的 package.json、自己的构建配置、自己的部署脚本。
如果我把文档转换器的接口加到商城后端里,结构会变成:
1 | mall-server/ |
看起来”只多了一个文件”。但半年后,这个目录会变成一个没人敢改的怪物。
独立项目的成本
说完了”为什么要独立”,也得说说”独立要付出什么成本”。
独立项目的成本是真实的:
- 多一个仓库:多一个 GitHub repo,多一套 git log,多一个 PR 流程
- 多一套 CI/CD:每个项目都要单独配部署脚本、单独配 PM2、单独配监控
- 多一个依赖管理:每个项目都有自己的
package.json,自己的node_modules,自己的版本升级 - 多一次部署:每个项目都要单独部署、单独回滚、单独验证
- 多一次维护:每个项目都有自己的 README、自己的架构图、自己的运维手册
对独立开发者来说,这些成本是真实的精力成本。
但代价的对比是这样的:
混用的成本:半年后,你有一个没人敢改的混合后端。它部署一次要同时考虑两个系统,修一个 bug 要同时测试两个功能,加一个新功能要同时改两个业务模块。它看起来”节省了一个仓库”,但实际上它在消耗你的认知带宽——每次改代码,你都要在脑子里同时维护两套业务模型。
独立的成本:你现在多花了半小时建一个新仓库、配一个新部署脚本、写一个新 README。但这半小时是一次性的。之后,每个项目都是独立的——你可以专注于一个系统,不用担心另一个系统的影响。
半小时的一次性成本,换半年后的认知带宽。
这笔账怎么算都不亏。
结论
不混用。
如果你的功能有自己的用户、有自己的数据模型、有自己的部署节奏——它就应该是独立项目。
如果你的功能没有自己的用户、没有自己的数据模型、没有自己的部署节奏——它可以是另一个项目的一个模块。
判断标准是三个问题:用户、数据模型、部署节奏。
我犯了错,你说了一句”不混用了吗”。
这句话值得记住。