程云来 / 杭州
Back to Blog·Spring
·About 6 min

Spring(六):好的架构,先解决什么问题

不从画大图或选框架开始,沿着创建订单的场景判断什么时候需要抽象、什么时候保持简单,以及如何找到架构方向。


先别急着画架构图

一个订单系统刚上线时,可能只有一个 API、一个数据库和三位开发者。半年后,订单、库存、支付和通知开始互相影响,发布一次小改动也要同时回归多个模块。

这时最容易出现两个极端:要么继续往一个项目里堆代码,要么立刻拆成多个微服务。两种做法都可能让问题更大,因为它们先回答了“采用什么结构”,却还没回答“当前最痛的约束是什么”。

架构工作的起点不是技术名词,而是一个可验证的问题:哪一种变化正在让系统变得难以修改、难以运行或难以恢复?

用四个问题找方向

面对一个新需求或一次重构,我会先写下四个答案:

  1. 谁在使用:用户、运营人员、其他系统,还是定时任务?
  2. 什么在变化:业务规则、外部依赖、流量、团队分工,还是数据规模?
  3. 哪里不能出错:重复扣款、数据丢失、越权访问,还是延迟超标?
  4. 如何验证:用测试、日志、指标、压测或一次可回滚的发布验证哪一个假设?

例如,支付渠道经常变化,重复扣款不能发生,且每次请求都要能追踪。此时需要优先设计支付接口、幂等键和审计记录,而不是先决定是否拆出支付微服务。

正在绘制图表…
查看 Mermaid 源码
flowchart TD
    problem[具体问题] --> change[识别变化来源]
    change --> constraint[写出不可违反的约束]
    constraint --> boundary[划分稳定边界]
    boundary --> smallest[选择最小可行结构]
    smallest --> verify[用测试和指标验证]
    verify -->|仍有问题| problem

Mermaid 大图

可滚动查看图表,点击缩放比例可恢复 100%。按 Esc 关闭。

图中的循环很重要:架构不是一次画完的终稿,而是假设、实现和反馈的循环。

什么时候应该做抽象

抽象的价值,是把反复出现的变化集中到一个位置。下面三种信号出现时,值得考虑接口、模块或模板:

  1. 同一规则在两个以上地方重复,修改一次需要同时改多处;
  2. 外部依赖正在变化,业务代码被 SDK、协议或供应商细节污染;
  3. 一个模块同时承担多个节奏,例如订单规则和消息投递必须独立发布或独立测试。

以支付为例,先抽出稳定的业务接口:

java
interface PaymentGateway {
    PaymentResult charge(PaymentCommand command);
}

class OrderService {
    private final PaymentGateway payment;

    OrderService(PaymentGateway payment) {
        this.payment = payment;
    }
}

这个接口隔离了支付渠道变化,但没有引入网络、消息队列或新的部署单元。先得到一个可替换的边界,再根据实际压力决定是否继续拆分。

什么时候不该做抽象

以下情况通常应该保持直接:

  1. 只有一个实现,且变化没有证据;
  2. 抽象层只是把参数原样转发,没有隐藏复杂度;
  3. 为了“未来可能扩展”提前引入配置、工厂和插件系统;
  4. 真实问题是数据模型、查询或错误处理,而不是模块数量。

例如,只有一个内部函数调用数据库,就不必先设计 RepositoryFactoryRepositoryProvider 和三层接口。多一层间接调用会增加阅读和调试成本,却没有提供可替换点。

判断一个抽象是否值得保留,可以问一句:

删除这层之后,哪一种已经发生的变化会重新扩散到多个地方?

如果答不出来,这层抽象可能只是形式上的复杂度。

模块、服务和平台不是同一个决定

架构决策有不同的成本等级,不应一次跳到最高等级:

决定解决的问题新增成本先做什么验证
类 / 接口职责混杂、实现不可替换命名和测试能否独立替换实现
模块边界发布节奏或依赖方向冲突构建与依赖管理模块能否独立测试
进程拆分隔离故障、独立扩缩容网络、部署、观测跨进程失败是否可恢复
平台化能力多团队重复建设运维和治理是否有多个真实使用方

从类和接口开始,并不意味着永远不拆服务;它只是让更昂贵的决定建立在已经观察到的约束上。

如何真正下手

不要从“设计完整架构”开始,可以先完成一个小闭环:

  1. 选一条真实用例,例如“创建订单并扣款”;
  2. 写出输入、输出、失败路径和需要保留的事实;
  3. 标出会变化的部分和必须稳定的部分;
  4. 只抽出一个能隔离已知变化的边界;
  5. 用一个成功测试和一个失败测试锁住行为;
  6. 记录这次选择的理由、代价和重新评估条件。
text
输入:订单号、金额、支付渠道
输出:支付结果、订单状态、审计记录
失败:超时、重复请求、渠道拒绝
稳定:订单状态转换和幂等规则
变化:支付渠道 SDK 和网络协议

在 Spring 中,这个闭环可能落成一个 OrderService、一个 PaymentGateway 和一个事务边界;不需要先引入微服务、事件总线或复杂的自动配置。

用 ADR 让方向可回看

当一个决定会影响多个模块或未来迁移成本时,写一页 Architecture Decision Record(ADR):

text
背景:支付渠道每季度更换一次 SDK
决定:OrderService 只依赖 PaymentGateway
候选:直接依赖 SDK / 引入接口 / 拆成独立服务
取舍:先增加一个接口和适配器,不增加网络调用
验证:替换 FakePaymentGateway 的测试通过
重新评估:出现独立扩缩容或故障隔离需求时再评估拆服务

ADR 的作用不是增加文档,而是把“为什么现在这样做”与“什么变化会让它失效”留下来。没有这两项,架构很容易变成没人敢改的历史惯例。

把 Spring 放回它该在的位置

Spring 提供 IoC、DI、AOP、模板和自动配置,但它们都是实现手段,不是架构方向本身:

text
先确认问题和约束
    ↓
划分稳定边界
    ↓
选择最小实现
    ↓
用 Spring 管理对象、生命周期和横切能力
    ↓
通过反馈决定是否继续抽象或拆分

好的架构不是组件最多、分层最齐,而是在当前约束下,让变化有地方放、错误有地方看、决定有证据回溯。下一次面对“要不要上某个框架”或“要不要拆服务”,先回到输入、输出、失败和变化四个问题。

参考:Spring Framework OverviewMADR Architecture Decision Records