乐于分享
好东西不私藏

架构师的文档修养:用ADR(Architecture Decision Records)记录关键技术决策

架构师的文档修养:用ADR(Architecture Decision Records)记录关键技术决策

你好,我是 Howell。

你们技术团队是否有自己的 Wiki 文档?是否记录架构演变历史和决策记录?接下来要谈一个让所有接盘侠(包括未来的你自己)都痛哭流涕的话题——架构决策文档。

1. 开篇:代码里的“考古学”悲剧

你有没有经历过这种场景?

你刚入职一家公司,接手了一个核心交易系统。你发现代码里有一个极其诡异的设计:所有的金额计算没有用 BigDecimal,而是用了 long 存分,这倒也没啥,但所有的数据库字段全是 VARCHAR

你跑去问团队里的老鸟:“为啥这核心金额字段是字符串?”老鸟吸了一口烟,眼神迷离:“三年前,架构师老王还在的时候,说是为了兼容某个老旧的COBOL外部接口,或者是为了防止精度丢失?反正老王走了,没人敢动。”

这就是典型的**“架构失忆症”**。

在软件工程中,代码只告诉了我们“它是这样做的(How)”,但永远不会告诉我们“为什么要这样做(Why)”。注释里很少写架构决策,Wiki 文档通常是三年前的,早就过时了。

当团队面临技术选型(比如选 RabbitMQ 还是 Kafka,选 Spring WebFlux 还是 Virtual Threads)时,往往是几次激烈的会议讨论,白板上画满了图。决定做完,大家散会写代码。

半年后,遇到性能瓶颈,你想改架构,却发现根本不知道当初为什么选了这个方案。是因为当时的技术限制?还是因为当时团队只会这个?还是因为某个政治原因?

这时候,你需要的是 ADR(Architecture Decision Records,架构决策记录)。

它不是那种几百页没人看的“软件架构说明书”,它是轻量级、跟随代码库、开发者友好的决策快照。今天,我们就来聊聊架构师的这项核心修养。


2. 正文解析:什么是 ADR 及其核心价值

ADR 是一种轻量级的文档格式,用于捕获重要的架构决策及其上下文和后果。

一个标准的 ADR 包含以下核心要素:

  1. 1. 标题:简短描述决策(如:ADR-001 使用 Java 21 虚拟线程替代 WebFlux)。
  2. 2. 状态:提议中、已通过、已废弃、已取代。
  3. 3. 背景(Context):我们面临什么问题?有什么约束?(例如:高并发IO密集型,但团队不熟悉响应式编程)。
  4. 4. 决策(Decision):我们决定做什么。
  5. 5. 后果(Consequences):决策带来的好处(Good)和坏处(Bad)。记住,所有架构决策都是Trade-off(权衡),没有完美的方案。

2.1 为什么要跟随代码库(Git)?

我见过太多公司把架构文档写在 Confluence、飞书文档或者 Word 里。结果就是:代码在演进,文档在腐烂。

ADR 的最佳实践是放在项目根目录的 /doc/adr 文件夹下,用 Markdown 编写,随 Git 提交。这样,代码回滚,决策记录也回滚;代码分支,决策也分支。Pull Request 时,架构决策也是 Code Review 的一部分。

我们先看一个 ADR 的生命周期图:


3. 实战案例:六个关键决策场景与 Java 21 代码落地

光说不练假把式。作为 Java 架构师,我用 6 个真实的生产环境场景,演示如何用 ADR 记录决策,并配合 Java 21/Spring Boot 3 的代码说明。

案例一:ADR-001 选择 Java 21 虚拟线程处理高并发 IO

背景我们的网关服务需要处理海量并发 HTTP 请求。之前的方案是 Spring WebFlux(Reactor),但团队反馈响应式编程调试极其困难,堆栈信息不可读,开发效率低。

决策使用 Spring Boot 3.2+ 搭配 Java 21 虚拟线程(Project Loom),抛弃 WebFlux,回归 Servlet 阻塞式编程模型。

代码落地在 Spring Boot 3 中开启虚拟线程非常简单。

// application.ymlspring:  threads:    virtual:      enabled: true

或者自定义 Executor(如果你需要更细粒度的控制):

package com.howell.architecture.config;import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.concurrent.Executors;@Configurationpublic class VirtualThreadConfig {    // 这是一个关键决策的落地代码    // 运行结果:Tomcat 将使用虚拟线程处理每个请求,    // 吞吐量在 IO 密集型场景下大幅提升,且保留了传统的同步编程模型。    @Bean    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {        return protocolHandler -> {            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());        };    }}

后果记录

  • • ✅ Good: 开发心智负担低,堆栈清晰,吞吐量接近 Reactive。
  • • ❌ Bad: 需要确保依赖库没有严重的 synchronized 块阻塞(Pinning 问题),需要升级大量中间件驱动。

案例二:ADR-002 使用 Java Record 承载 DTO 与不可变数据

背景项目中充斥着大量的 Lombok @Data 注解,导致数据在传递过程中被随意修改,引发了线程安全问题和逻辑混乱。

决策所有内部模块交互的 DTO(Data Transfer Object)强制使用 Java 16+ 引入的 record 关键字。

代码落地

package com.howell.architecture.dto;import java.time.LocalDateTime;// 这是一个关键决策:使用 Record 定义不可变数据载体// 运行结果:自动生成构造器、getter、equals、hashCode、toString。// 且字段默认 final,无法被修改,保证了数据流转的安全性。public record UserTransactionEvent(    String transactionId,    String userId,    long amountInCents,    LocalDateTime timestamp,    TransactionType type) {    // 紧凑构造器进行校验    public UserTransactionEvent {        if (amountInCents < 0) {            throw new IllegalArgumentException("金额不能为负数");        }    }}enum TransactionType { DEPOSIT, WITHDRAW }

后果记录

  • • ✅ Good: 线程安全,代码简洁,序列化性能好。
  • • ❌ Bad: 部分老旧的序列化框架(如某些版本的 Fastjson)支持不完善,需升级 Jackson。

案例三:ADR-003 模块化单体(Modulith)替代微服务起步

背景新业务线刚起步,业务边界模糊。直接上微服务会导致分布式事务泛滥,运维成本过高。

决策采用 Spring Modulith 构建“模块化单体”。在同一个 JVM 内通过 ApplicationEvent 解耦模块,保留未来拆分微服务的能力。

代码落地

package com.howell.architecture.order;import org.springframework.modulith.ApplicationModuleListener;import org.springframework.stereotype.Component;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@Componentpublic class InventoryListener {    private static final Logger log = LoggerFactory.getLogger(InventoryListener.class);    // 关键决策:模块间通信通过事件,而不是直接的方法调用    // 运行结果:当订单模块发布 OrderPlacedEvent 时,库存模块异步或同步响应。    // Spring Modulith 会强制检查模块间的依赖可视性。    @ApplicationModuleListener    public void on(OrderPlacedEvent event) {        log.info("接收到订单事件,准备扣减库存: {}", event.orderId());        // 业务逻辑...    }}// 定义在 order 包下的事件record OrderPlacedEvent(String orderId) {}

后果记录

  • • ✅ Good: 部署简单,无网络开销,重构边界容易。
  • • ❌ Bad: 代码库庞大,编译时间变长。

案例四:ADR-004 使用 JdbcClient 替代复杂的 ORM

背景项目中存在大量复杂的报表查询,JPA/Hibernate 生成的 SQL 难以优化,MyBatis XML 维护繁琐。

决策对于复杂查询和报表模块,使用 Spring 6.1 引入的 JdbcClient(Fluent API),放弃 MyBatis。

代码落地

package com.howell.architecture.repository;import org.springframework.jdbc.core.simple.JdbcClient;import org.springframework.stereotype.Repository;import java.util.List;@Repositorypublic class ReportRepository {    private final JdbcClient jdbcClient;    public ReportRepository(JdbcClient jdbcClient) {        this.jdbcClient = jdbcClient;    }    // 关键决策:使用流式 API 编写原生 SQL    // 运行结果:直观的 SQL 控制,自动映射到 Record,性能极致。    public List<DailyReport> getDailyReports(String date) {        return jdbcClient.sql("SELECT category, SUM(amount) as total FROM transactions WHERE tx_date = :date GROUP BY category")                .param("date", date)                .query(DailyReport.class)                .list();    }}record DailyReport(String category, long total) {}

后果记录

  • • ✅ Good: SQL 可控,无缓存黑盒,启动速度快。
  • • ❌ Bad: 失去了 JPA 的级联更新和脏检查功能(但这正是我们想要的,显式控制)。

案例五:ADR-005 统一异常处理与 RFC 7807 标准

背景前后端联调时,异常格式五花八门,有的返回 HTTP 200 + code: 500,有的直接抛堆栈。

决策全站采用 RFC 7807 (Problem Details for HTTP APIs) 标准,并利用 Spring Boot 3 的 ErrorResponse 接口。

代码落地

package com.howell.architecture.exception;import org.springframework.http.HttpStatus;import org.springframework.http.ProblemDetail;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.net.URI;@RestControllerAdvicepublic class GlobalExceptionHandler {    // 关键决策:标准化错误返回    // 运行结果:前端收到标准的 JSON 格式,包含 type, title, status, detail, instance    @ExceptionHandler(IllegalArgumentException.class)    public ProblemDetail handleArgumentError(IllegalArgumentException e) {        ProblemDetail problem = ProblemDetail.forStatusAndDetail(            HttpStatus.BAD_REQUEST,             e.getMessage()        );        problem.setTitle("参数校验失败");        problem.setType(URI.create("https://api.howell.com/errors/bad-request"));        problem.setProperty("timestamp", System.currentTimeMillis()); // 扩展字段        return problem;    }}

后果记录

  • • ✅ Good: 符合国际标准,利于 API 网关统一拦截监控。
  • • ❌ Bad: 需要前端修改统一拦截器适配新格式。

案例六:ADR-006 缓存一致性策略 – 旁路缓存(Cache Aside)

背景用户中心数据频繁读取,但偶尔修改。之前使用了 @Cacheable 注解,但出现过数据库回滚而缓存未清除的脏数据问题。

决策放弃 Spring Cache 注解的“黑盒”模式,手动实现 Cache Aside 模式,并引入“延迟双删”或“Binlog 订阅”机制(视一致性要求而定)。这里演示手动控制。

代码落地

package com.howell.architecture.service;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;@Servicepublic class UserProfileService {    private final StringRedisTemplate redisTemplate;    private final UserRepository userRepo;    // ... 构造器注入    // 关键决策:显式控制缓存逻辑    // 运行结果:先更库,后删缓存。虽然有极低概率并发问题,但比注解更可控。    public void updateUserProfile(String userId, String newName) {        // 1. 更新数据库        userRepo.updateName(userId, newName);        // 2. 删除缓存 (Cache Aside Pattern)        // 实际生产中可能需要配合消息队列进行重试删除        redisTemplate.delete("user:" + userId);    }}

后果记录

  • • ✅ Good: 逻辑清晰,不再被 Spring Cache 的 AOP 坑(如内部调用失效)。
  • • ❌ Bad: 代码量增加,业务逻辑混杂了缓存逻辑。

4. 逻辑图解:ADR 决策树

为了让大家更直观地理解如何做决策,我们用 画一个关于“并发模型选择”的决策逻辑图。这是架构师脑子里的回路,现在我们把它具象化。


5. 思维拓展:架构师的“邪修”之道

讲完正统的 ADR,Howell 必须给你们讲点“邪修”版本(Dark Side of Architecture)。

在职场中,ADR 有时候不只是为了技术,更是为了**“甩锅”“防御性架构”**。

  1. 1. 护身符模式当业务方逼你上线一个明显有坑的功能(比如“双十一前一周重构核心链路”),你拦不住。这时候,写一个 ADR,详细列出 Consequences 中的 Risks(风险):可能导致宕机、数据不一致。让老板在 Pull Request 上点 Approve。一旦炸雷,这个 ADR 就是你的免死金牌:“看,我当时明确警告过风险,是你们坚持要上的。”
  2. 2. 反向卷王模式有些架构师为了显得自己牛逼,引入了极其复杂的技术栈(比如 K8s + Service Mesh + GraphQL),但团队只有 5 个人。他在 ADR 里写满了“高可扩展性”、“云原生未来”。实际上,这是为了他自己的简历镀金。这种 ADR 我们称之为 RDD (Resume Driven Development,简历驱动开发)兄弟们,千万别这么干,这是坑队友。 真正的架构师,是用最简单的方案解决最复杂的问题
  3. 3. 常见误区
    • • 补作业:项目做完了才去写 ADR。那不叫决策记录,那叫回忆录。
    • • 写作文:一个 ADR 写了 5000 字。没人看的。ADR 必须短小精悍,直击痛点。

6. 总结

架构不是一蹴而就的设计,而是一连串决策的累积。

代码决定了系统现在的样子,而 ADR 记录了系统为什么长成这个样子。

对于想要进阶的 Java 开发者,我的建议是:

  1. 1. 从今天开始,在你的项目里建一个 /doc/adr 目录。
  2. 2. 不要追求完美,哪怕只记录一个“为什么我们用 MySQL 而不是 PostgreSQL”,也是巨大的进步。
  3. 3. 用代码思维写文档,Markdown + Git,让文档活在代码库里。

Takeaway (今日要点):

一流的架构师,不仅能写出高性能的代码,更能通过清晰的文档(ADR),管理技术债务,统一团队认知,防止系统在人员更迭中发生“架构腐烂”。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 架构师的文档修养:用ADR(Architecture Decision Records)记录关键技术决策

评论 抢沙发

5 + 6 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮