我在 wiki 裡看過的每一張架構圖都是錯的。不是明顯的錯,而是安靜地、漸進地錯。標示為「Auth」的服務在六個月前就被拆成三個微服務。標示為「sync call」的箭頭現在已經透過 queue 變成 async。標示為「PostgreSQL」的資料庫在某次火災演練中被遷移到別的東西,但沒人更新那個方框。

這不是疏忽。這是物理定律。程式碼每週改動數百次。文件只在有人想起來時才更新。架構圖的半衰期大約是一個 sprint。過了這段時間,它就變成小說。

目標不是寫出永遠不會偏離的文件。目標是在它誤導下一位工程師之前,偵測到這種偏離。

為什麼文件比程式碼更快腐敗

程式碼有內建的修正機制:它要嘛編譯過且通過測試,要嘛就不行。文件沒有編譯器、沒有測試套件、沒有 CI 閘門。一張錯誤的圖表可以完美呈現。一份過時的 ADR 讀起來跟新鮮的一樣有說服力。

這種分歧也是社會性的。工程師更新程式碼是因為建置失敗。他們更新文件是因為感到內疚。內疚是一個不可靠的scheduler。

當新進工程師加入並閱讀 wiki 時,他們會建立對系統的心智模型。如果那個模型已經過時六個月,他們做出的決定會讓現有的混亂更加複雜。原本應該減少 onboarding 摩擦的文件,變成了微妙且代價高昂的誤導來源。

三個虛構陷阱

架構文件最快變成虛構的地方有三個。

Wiki 墳場。 Confluence、Notion 或 SharePoint 裡的架構文件存在於 repo 之外。它們沒有與程式碼變更相關聯的 commit 歷史。當重構刪除了一個服務,wiki 頁面像數位鬼城一樣存活下來。沒有建置失敗。沒有警報響起。

圖表虛構。 從 Lucidchart 或 draw.io 匯出並貼到 README 裡的精美圖表看起來很有權威性。但它也是一張靜態圖片。下一位改變架構的開發者必須打開設計工具、找到原始檔、更新它、重新匯出、再貼回去。那個工作流程的生存率是 5%。

全知 README。 頂層的 README 試圖在一個檔案裡記錄每個服務、每個相依關係、每個資料流。它變成一份沒人敢碰的巨型文件。最終,某個勇敢的人加了一條註解:「這個章節可能已經過時。」然後又加一條。然後整份文件被引號包圍並被遺棄。

保持誠實的動態文件

解決方案不是寫更多文件。而是讓文件無法被迴避。

把文件搬進 repository。 如果文件不在與程式碼變更相同的 pull request 裡,它就不會被更新。ADR、runbook 和決策日誌應該放在它們所描述的程式碼旁邊,例如 docs/architecture/docs/adr/。當審查者看到重構沒有更新文件時,他們可以擋下 merge。

從程式碼產生圖表。 Mermaid、PlantUML 和 Structurizr 讓你用文字檔定義圖表,這些檔案存在於 repo 中。CI job 在每次 commit 時渲染它們。當架構改變時,圖表原始碼也隨之改變。不需要設計工具。

<!-- docs/architecture/data-flow.md -->
## Ingestion Pipeline

```mermaid
graph LR
    A[Client SDK] -->|HTTP POST| B[Ingest API]
    B -->|Pub/Sub| C[Event Consumer]
    C -->|Write| D[(ClickHouse)]

這張圖表可以在 diff 中審查。當 pipeline 增加了一個新的 Kafka topic,變更會出現在新增 consumer 的同一個 PR 中。

**把「什麼」和「為什麼」分開。** 程式碼庫已經描述了系統做什麼。註解、函式名稱和型別編碼了目前的結構。文件應該聚焦於系統為什麼長成這樣。這才是會悄然偏離的東西:決策背後的推理,而不是決策本身。

Architecture Decision Record 捕捉了這一點。ADR 不是規格文件。它是一份帶時間戳的筆記,寫著:「在這個日期,基於這些理由,我們選擇了這個。這些是我們接受的限制條件。」當限制條件改變時,ADR 會被新的取代。舊的記錄保留作為歷史。

## 自動化「什麼」,手寫「為什麼」

最誠實的架構文件是你不用手寫的那些。

從 controller 程式碼產生的 OpenAPI spec 準確地描述了你的 API 表面。TypeDoc、RustDoc 或 Javadoc 描述你的內部型別。`cargo tree`、`go mod graph` 或 `webpack-bundle-analyzer` 產生的 dependency graph 展示了實際的模組結構。

對於更高層級的架構限制,自動化架構測試扮演安全網的角色。在 Java 中,ArchUnit 可以強制執行諸如「`domain` 中的 package 不得相依於 `infrastructure`」這類規則。在 Python 中,`import-linter` 做同樣的事。在 Go 中,你可以寫一個小測試來遍歷 AST,如果出現 forbidden import 就讓建置失敗。

```java
// This test fails the build if architecture rules break.
@ArchTest
static final ArchRule domain_independence =
    noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat()
        .resideInAPackage("..infrastructure..");

當開發者不小心 import 了錯誤的 package,建置會立即失敗。架構文件不需要警告他們。編譯器會。

沒人願意承認的取捨

動態文件不是免費的。它花費審查時間、CI 分鐘數,以及偶爾爭論某個變更是否值得一份新的 ADR。

不是每個系統都需要圖表。 一個只有三層的單體 monolith 不需要 C4 model。維護產生文件的努力應該與誤解系統的痛苦成正比。如果錯誤的心智模型讓你花一天除錯,那就投資一小時畫一張圖表。如果只需要五分鐘,那就寫一則註解。

產生的文件也會說謊。 從程式碼產生的 OpenAPI spec 描述了每個 endpoint,包括你忘記記錄的內部 endpoint。Dependency graph 展示了每個 import,包括那些違反你架構的 import。產生的真相是準確的,但不見得有用。你仍然需要人為策展來凸顯什麼才是重要的。

ADR 會累積。 兩年後,你可能在 docs/adr/ 裡有三十份 ADR。大多數會變得無關。解決方案不是刪除它們。而是清楚地標示:status: superseded 並附上連結到取代它的文件。歷史有價值。困惑沒有。

一個可行的小型設定

你不需要文件平台。你只需要在 repo 裡放三樣東西。

  1. docs/adr/YYYY-MM-DD-title.md 用於決策。使用輕量模板:
# ADR 012: Event Storage Backend

- Status: accepted
- Date: 2026-03-15

## Context
We need durable storage for ingestion events with sub-second query latency.

## Decision
Use ClickHouse for hot storage, S3 for cold archival.

## Consequences
- Fast analytical queries on recent data.
- Complex operational footprint compared to PostgreSQL.
- Migration path defined in ADR 013.
  1. docs/architecture/*.md 包含 Mermaid 圖表,用於系統邊界和資料流。要求結構性程式碼變更時在同一個 PR 中更新文件。

  2. 一個架構測試 用來強制執行你最昂貴的不變條件。如果你只有一條規則,就讓它防止上次那個痛苦的重構。隨著系統成長再增加更多。

常見問題

我需要為每個微服務畫圖表嗎?

不需要。為團隊交接的邊界、資料跨越信任區域的地方、以及故障連鎖反應的地方畫圖表。你內部 CRUD 服務的圖表通常是浪費。哪些服務呼叫 payment gateway 的圖表通常很有價值。

如果我的團隊已經在使用 Confluence 怎麼辦?

在 repo 中鏡射關鍵文件並連結出去。真相來源留在 git 裡。Wiki 變成便利層。如果 wiki 偏離了,工程師知道去哪裡找真正的版本。

多少份 ADR 才算太多?

沒有上限,但可讀性有極限。如果新進工程師無法在十分鐘內掃過 docs/adr/ 並理解系統的演進,那就加一份索引。按子系統分組。清楚地標示已取代的記錄。

給非技術利害關係人的 wiki 呢?

產品經理和高階主管需要高層級摘要。從 ADR 索引產生這些摘要。不要維護一套獨立的敘事。同樣的事實應該從原始碼流出,經過 ADR,進入人類可讀的摘要。一個真相來源,多種視角。

從一份 ADR 和一個測試開始

你不需要在這週就徹底改變你的文件文化。挑選最後一個造成困惑的架構決策。寫一份 ADR 解釋你為什麼選擇了那個方案。挑選一個你希望自動強制執行的不變條件。寫一個架構測試,當有人違反時就讓建置失敗。

這兩個產物會保持誠實,因為它們存在於 repo 中、在 PR 中被審查、而且它們要嘛通過 CI,要嘛就不會上線。其他的一切——wiki 頁面、投影片、會議海報——都是次要的。有很好。可能有點用。但很可能是錯的。