# 跨邊界的語意流失，與 monolith 一直有吸引力的原因

> 大型系統難組合，根因多半是語意在跨服務邊界被壓成 bytes，型別系統與重構工具接不住。monolith 的吸引力在於語意留在同一套工具鏈裡，拆之前先算這條邊界要犧牲多少語意。

原文：https://sheng.page/posts/semantics-across-boundaries-and-the-monolith/ · 發布：2025-05-10 · 作者：Sheng

每次接手一個拆得很細的系統，過一陣子都會冒出同一句抱怨：明明每個服務都寫得不差，組起來卻像一坨難動的東西。直覺會去怪某個寫爛的 service，但多數時候根因不在那。真正掉的東西是語意，它在跨服務邊界的那一刻被壓扁了。

## 邊界把語意壓成 bytes

在同一個 process 裡，`Order` 的欄位、不變條件，以及它跟 `Payment` 的關係，都活在編譯器看得到的範圍內。結構一改，工具會立刻指出受影響的 caller；重構、改名，或把欄位從 optional 改成 required 時，語意仍有人幫忙盯著。

跨過服務邊界後，`Order` 會先被序列化成 JSON、protobuf 或其他 wire format，到對面再反序列化；傳輸途中只剩一串 bytes，原本的語意得靠契約補回來。那份契約可能是不一定同步的 OpenAPI、一段 README，或某個工程師三年前在 Slack 留下的說明。型別系統看不到邊界另一端，自然無法替兩邊核對。

原本由工具自動處理的工作，過了邊界就退回成人工。欄位改名時編譯器不會報錯，下游可能要等到 runtime 收到某筆資料才壞；要確認欄位還有沒有人使用，也沒有 IDE 的 find usages，只能翻 log、問人，再承擔猜錯的風險。資料結構遷移因此從一次可驗證的重構，變成跨團隊協調與逐步上線。語意仍然存在，只是離開工具能驗證的地方，改由人腦追蹤。

## monolith 的吸引力不是懶

這也說明了 monolith 為什麼一直有人想回去：語意能留在同一套型別系統與工具鏈裡，並不等於團隊在技術上偷懶或不懂分散式。

在一個 monolith 裡，從 HTTP handler 一路到資料層，`Order` 都還是那個 `Order`。改它的定義，編譯器陪你走完每一個 caller；想拔掉一個欄位，find usages 告訴你還有誰在碰；要把一段邏輯搬家，IDE 的 refactor 一鍵到位。別把這些當小確幸，大型系統還能持續演進，靠的就是它們。系統老化源於你逐漸不敢動它。monolith 把「敢動」這件事的成本壓得很低，因為工具一直站在你這邊。

這也解釋了為什麼有些團隊拆成 microservices 之後反而更慢。他們以為買到的是獨立部署跟團隊自治，實際上付掉的，是原本白白享有的那層語意驗證。每一條服務邊界，都是一個工具不再幫你把關的地方。邊界開得越多，落回人工協調的面積就越大。一個 well-factored 的 monolith，常常比一套邊界亂畫的 microservices 好維護得多，差別就在語意還在不在工具的射程內。

## 拆之前先問值不值

講這些不是要你別拆服務。有些邊界該拆，理由很硬：團隊要獨立部署不想互相卡、某段工作負載的 scaling 特性跟其他人完全不同、某塊東西的故障必須隔離不能拖垮全站。這些都是好理由，該拆就拆。

重點是順序。拆之前先直說一句：這條邊界，要我犧牲多少語意換？如果一條邊界把兩個高度耦合、天天一起改的東西切開，你等於是把編譯器原本幫你頂住的工作，整碗倒回給跨團隊的人工協調，這筆帳通常不划算。

就算決定要拆，語意也不是只能認賠。它可以靠 shared model 接回來一部分：跨邊界共用的 schema、生出多語言型別的 IDL、還有契約測試，讓 provider 一改壞掉原有結構，CI 就先替 consumer 喊痛，而不是等 production 的某筆資料去發現。這些東西不會讓跨服務變得跟同 process 一樣安全，但它們是在邊界上重新長出一點工具支援，把語意從純人工的深淵拉回來一些。差別在於，你是有意識地在補這層，還是假裝邊界不用成本。

邊界適合畫在語意損失最小的地方，沿著耦合最弱、契約最穩定的縫切開，跨邊界要交換的意義較少，工具接不住的部分也會縮小。若只為對齊團隊結構或讓架構圖好看，硬把兩塊高度相關的東西拆開，得到的往往是一套長期需要補洞的分散式系統。


## 補充筆記

- 大型系統難組合，根因常是語意在跨邊界被壓成 bytes 與隱含契約，不是某個元件爛。
- 過了服務邊界，型別系統就失效，驗證、重構、遷移全退回人工，錯誤被推遲到 runtime。
- monolith 的吸引力在於語意還活在同一套工具鏈裡，工具能幫你驗證與重構，跟偷懶無關。
- 拆服務前先問這條邊界值得犧牲多少語意；shared schema 與契約測試能接回一部分。
- 邊界要畫在語意損失最小的地方，沿最弱的耦合縫切，不是照組織圖好看的地方切。
> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 列入 20260613 blog 翻新計劃，新漆未乾。