先别急着画架构图
一个订单系统刚上线时,可能只有一个 API、一个数据库和三位开发者。半年后,订单、库存、支付和通知开始互相影响,发布一次小改动也要同时回归多个模块。
这时最容易出现两个极端:要么继续往一个项目里堆代码,要么立刻拆成多个微服务。两种做法都可能让问题更大,因为它们先回答了“采用什么结构”,却还没回答“当前最痛的约束是什么”。
架构工作的起点不是技术名词,而是一个可验证的问题:哪一种变化正在让系统变得难以修改、难以运行或难以恢复?
用四个问题找方向
面对一个新需求或一次重构,我会先写下四个答案:
- 谁在使用:用户、运营人员、其他系统,还是定时任务?
- 什么在变化:业务规则、外部依赖、流量、团队分工,还是数据规模?
- 哪里不能出错:重复扣款、数据丢失、越权访问,还是延迟超标?
- 如何验证:用测试、日志、指标、压测或一次可回滚的发布验证哪一个假设?
例如,支付渠道经常变化,重复扣款不能发生,且每次请求都要能追踪。此时需要优先设计支付接口、幂等键和审计记录,而不是先决定是否拆出支付微服务。
查看 Mermaid 源码
flowchart TD
problem[具体问题] --> change[识别变化来源]
change --> constraint[写出不可违反的约束]
constraint --> boundary[划分稳定边界]
boundary --> smallest[选择最小可行结构]
smallest --> verify[用测试和指标验证]
verify -->|仍有问题| problem图中的循环很重要:架构不是一次画完的终稿,而是假设、实现和反馈的循环。
什么时候应该做抽象
抽象的价值,是把反复出现的变化集中到一个位置。下面三种信号出现时,值得考虑接口、模块或模板:
- 同一规则在两个以上地方重复,修改一次需要同时改多处;
- 外部依赖正在变化,业务代码被 SDK、协议或供应商细节污染;
- 一个模块同时承担多个节奏,例如订单规则和消息投递必须独立发布或独立测试。
以支付为例,先抽出稳定的业务接口:
interface PaymentGateway {
PaymentResult charge(PaymentCommand command);
}
class OrderService {
private final PaymentGateway payment;
OrderService(PaymentGateway payment) {
this.payment = payment;
}
}这个接口隔离了支付渠道变化,但没有引入网络、消息队列或新的部署单元。先得到一个可替换的边界,再根据实际压力决定是否继续拆分。
什么时候不该做抽象
以下情况通常应该保持直接:
- 只有一个实现,且变化没有证据;
- 抽象层只是把参数原样转发,没有隐藏复杂度;
- 为了“未来可能扩展”提前引入配置、工厂和插件系统;
- 真实问题是数据模型、查询或错误处理,而不是模块数量。
例如,只有一个内部函数调用数据库,就不必先设计 RepositoryFactory、RepositoryProvider 和三层接口。多一层间接调用会增加阅读和调试成本,却没有提供可替换点。
判断一个抽象是否值得保留,可以问一句:
删除这层之后,哪一种已经发生的变化会重新扩散到多个地方?
如果答不出来,这层抽象可能只是形式上的复杂度。
模块、服务和平台不是同一个决定
架构决策有不同的成本等级,不应一次跳到最高等级:
| 决定 | 解决的问题 | 新增成本 | 先做什么验证 |
|---|---|---|---|
| 类 / 接口 | 职责混杂、实现不可替换 | 命名和测试 | 能否独立替换实现 |
| 模块边界 | 发布节奏或依赖方向冲突 | 构建与依赖管理 | 模块能否独立测试 |
| 进程拆分 | 隔离故障、独立扩缩容 | 网络、部署、观测 | 跨进程失败是否可恢复 |
| 平台化能力 | 多团队重复建设 | 运维和治理 | 是否有多个真实使用方 |
从类和接口开始,并不意味着永远不拆服务;它只是让更昂贵的决定建立在已经观察到的约束上。
如何真正下手
不要从“设计完整架构”开始,可以先完成一个小闭环:
- 选一条真实用例,例如“创建订单并扣款”;
- 写出输入、输出、失败路径和需要保留的事实;
- 标出会变化的部分和必须稳定的部分;
- 只抽出一个能隔离已知变化的边界;
- 用一个成功测试和一个失败测试锁住行为;
- 记录这次选择的理由、代价和重新评估条件。
输入:订单号、金额、支付渠道
输出:支付结果、订单状态、审计记录
失败:超时、重复请求、渠道拒绝
稳定:订单状态转换和幂等规则
变化:支付渠道 SDK 和网络协议在 Spring 中,这个闭环可能落成一个 OrderService、一个 PaymentGateway 和一个事务边界;不需要先引入微服务、事件总线或复杂的自动配置。
用 ADR 让方向可回看
当一个决定会影响多个模块或未来迁移成本时,写一页 Architecture Decision Record(ADR):
背景:支付渠道每季度更换一次 SDK
决定:OrderService 只依赖 PaymentGateway
候选:直接依赖 SDK / 引入接口 / 拆成独立服务
取舍:先增加一个接口和适配器,不增加网络调用
验证:替换 FakePaymentGateway 的测试通过
重新评估:出现独立扩缩容或故障隔离需求时再评估拆服务ADR 的作用不是增加文档,而是把“为什么现在这样做”与“什么变化会让它失效”留下来。没有这两项,架构很容易变成没人敢改的历史惯例。
把 Spring 放回它该在的位置
Spring 提供 IoC、DI、AOP、模板和自动配置,但它们都是实现手段,不是架构方向本身:
先确认问题和约束
↓
划分稳定边界
↓
选择最小实现
↓
用 Spring 管理对象、生命周期和横切能力
↓
通过反馈决定是否继续抽象或拆分好的架构不是组件最多、分层最齐,而是在当前约束下,让变化有地方放、错误有地方看、决定有证据回溯。下一次面对“要不要上某个框架”或“要不要拆服务”,先回到输入、输出、失败和变化四个问题。
参考:Spring Framework Overview、MADR Architecture Decision Records