Autotrader 团队采用模块化架构结合六边形设计,构建高内聚低耦合的金融系统,兼顾开发效率与长期可维护性。
本文由 Emina Cholich 与 Craig Shipton 联合撰写。Emina 是英国汽车交易平台 Autotrader 的资深工程师,专注于构建高内聚、可演进的后端系统;Craig 则是该团队的技术负责人,长期深耕金融集成领域,擅长在工程效率与系统稳定性之间寻找最优解。他们所在的团队负责 Autotrader 平台上车辆金融报价与贷款申请的核心服务,需对接 70 多家金融机构、20 多种异构 API,技术复杂度极高。
在软件开发这条路上,我们总在“简单”和“混乱”之间反复横跳。 早期的单体应用,部署快、调试爽,但随着功能越堆越多,代码就像一锅乱炖——改一个按钮,可能崩掉整个贷款流程。 后来大家一窝蜂转向微服务,以为拆得越碎越高级,结果发现:维护二十多个服务,光是日志对齐、链路追踪、网络超时,就能把人熬成秃头。
那有没有一种架构,既能享受单体的部署简单,又能拥有微服务的清晰边界?
有!它叫——模块化(Modulith)。
别被名字吓到,模块化说白了,就是一个“有纪律的单体”。
整个应用还是一个整体打包、一键部署的单元,但内部被严格划分为多个独立模块。每个模块:
- 拥有自己的业务职责
- 管理自己的数据表
- 通过明确定义的接口和其他模块对话
绝不允许偷偷调用对方的内部类,更不允许跨模块直接操作数据库!
这样一来,它既避开了传统单体“牵一发而动全身”的噩梦,又省去了微服务那套繁重的运维负担。
简单说:部署像单体,结构像微服务。
那 Autotrader 团队为啥选它?故事得从他们的业务说起。
他们要做一个“车辆金融报价系统”——用户在 Autotrader 上选好车,点一下“申请贷款”,系统就得把客户信息、车型、首付、贷款期限等数据,精准发送给合作的金融机构。
听起来简单?实际复杂到爆!
他们要对接 70 多家金融机构,背后是 20 多种完全不同的 API:
- 有的用 REST,有的还在用古老的 SOAP
- 有的要先做软信用查询,有的要计算车辆残值
- 有的流程三步搞定,有的要七步审批加人工复核
更麻烦的是,所有这些流程都共享一套核心数据模型,比如“贷款申请”这个概念。 如果代码乱写,A 银行的逻辑混进 B 金融公司的代码里,不出三个月,整个系统就变成“谁碰谁死”的禁区。
面对这种局面,团队认真评估了两种主流架构:
选项一:传统单体
优点:部署简单,数据库事务强一致,开发调试快。
缺点:随着 lender 越接越多,代码耦合度飙升,新人进来三天就崩溃。
选项二:微服务
每个金融机构一个服务,边界清晰,团队可并行开发。
但问题来了:20 多个服务,意味着 20 套 CI/CD、20 套监控告警、20 套数据库连接池……长期维护成本高到离谱。
于是他们拍板:走第三条路——模块化架构!
既能并行开发不同金融机构的对接逻辑,又不用背负微服务的运维重担。
但光有“模块化”这个概念还不够,怎么确保模块之间真的“干净”?
这时候,他们祭出了另一个架构利器——六边形架构(也叫“端口与适配器”模式)。
六边形架构的核心思想是:把核心业务逻辑放在“神圣不可侵犯”的中心,所有外部依赖——数据库、API、消息队列——都只能通过“端口”进出。
举个例子:
核心逻辑说:“我需要发送一个贷款申请。”
这叫端口——它只是一个接口,不关心谁来实现。
而针对某家银行的 SOAP 接口写的具体调用代码,就是适配器。
这样一来,核心业务完全不知道外面是 REST 还是 SOAP,测试时随便 mock 一个适配器就行,根本不用连真实 API。
业务逻辑干净、可测、与技术解耦。
模块化 + 六边形架构,一个管“内部结构”,一个管“内外边界”,简直是天作之合。
Autotrader 团队正是靠这套组合拳,搭出了一个既清晰又灵活的系统。
他们的代码结构长这样:
最核心的是一个叫 hexagon 的模块——这是整个系统的“宪法”。
里面放着:
- 所有领域模型(比如 Proposal、Customer、Vehicle)
- 所有端口定义(比如 sendProposal()、checkCredit())
- 通用异常、验证器等跨模块工具
这个模块没有任何外部依赖,反而是其他所有模块都依赖它。
然后是 service 模块,负责处理客户端请求。
它包含 REST 控制器,也包含整个贷款申请的生命周期编排逻辑:
- 先验证用户信息
- 再调用信用检查
- 然后计算月供
- 最后把申请发给对应的金融机构
这个模块会操作自己的数据库表,记录申请进度。但一旦需要调用外部服务,它绝不直接写 HTTP 请求,而是通过 hexagon 模块里定义的端口来调用。
真正的魔法发生在适配器模块。
比如“金融机构对接”这一大类,每个 lender 都有自己的独立子模块:
- A银行一个模块
- B金融公司一个模块
- C信贷机构又一个模块
每个模块都包含:
- 自己的数据模型(只在本模块内使用)
- 自己的数据库 schema(在同一个 PostgreSQL 实例里,但表完全隔离)
- 自己的 API 调用逻辑(SOAP、REST、文件上传,各玩各的)
最关键的是:当 service 模块要发送申请时,它传给端口的是 hexagon 定义的标准 Proposal 对象;而 lender 模块在内部把它转换成自己需要的格式,调用 API 后,再把结果转回 hexagon 定义的标准 Response 对象返回。
整个过程,没有任何 lender 模块知道其他 lender 的存在,也没有任何 lender 的细节泄露到核心逻辑里。
除了金融机构,还有一些“辅助服务”也被拆成独立模块,比如:
- 残值计算服务
- 软信用查询服务
这些虽然是内部系统,但也是外部依赖,所以同样用“端口+适配器”封装,确保核心逻辑不受干扰。
这种设计还有一个隐藏大招:未来可演进。
如果某家金融机构的流量突然暴涨,或者它的 API 特别不稳定,团队完全可以把这个 lender 模块整个抽出来,变成一个独立部署的微服务——而核心逻辑几乎不用改!因为端口已经定义好了,换适配器就行。
技术实现上,他们用的是 Gradle 多项目构建(multi-project build),每个模块都是一个子项目,依赖关系通过 buildSrc 严格控制。这样在编译阶段就能拦截非法依赖,比如 lender 模块绝不能直接 import service 模块的类。
总结一下:
Autotrader 团队没有盲目追风微服务,而是冷静分析业务后,选择了模块化 + 六边形架构的务实组合。 他们用清晰的模块边界、严格的接口契约、统一的领域模型,成功在一个单体应用里实现了微服务级别的解耦。 这不仅让多个团队能并行开发不同金融机构的对接,也让代码库从第一天起就保持整洁、可测试、可维护。
当然,他们也坦言:这种架构不是银弹。 比如模块划分需要极强的领域理解,初期设计成本较高;数据库虽用多 schema 隔离,但仍是单实例,存在扩展瓶颈。 但对于他们这种“中等复杂度、强事务一致性、多外部集成”的场景,模块化显然是更聪明的选择。
在软件架构的世界里,没有永远正确的答案,只有最适合当下问题的解法。 模块化不是倒退回单体,而是用现代工程思维重新定义了“单体”的可能性——它不是妥协,而是一种更高级的平衡。