本章概覽#
第 5 章從五個品質屬性(可修改性、可演化性、可測試性、可部署性、可觀測性)角度描述了流暢交付的架構需求。本章專注於單一元件架構——巨石架構(monolithic architecture)——說明它如何滿足這些品質屬性,以及如何用設計手法填補巨石未明確規範的部分,以提升團隊自主性與部署管線速度,重點是模組化巨石(modular monolith)模式。
巨石並非反模式。對許多應用而言,它能避開分散式架構固有的複雜度。重點是知道什麼時候它適用、什麼時候會成為流暢交付的阻礙,以及如何把它做得更好。
本章將討論的主題#
- 巨石架構的優點與限制
- 巨石滿足流暢交付五個品質屬性的能力
- 結構化巨石以提升團隊自主性的策略
- 減少建置期耦合、加速部署管線的設計手法
巨石架構是什麼#
巨石架構是把應用結構為單一元件的簡單架構風格:
- 程式碼放在單一程式碼倉(repository)。
- 由單一部署管線建置與部署。
- 通常配一個關聯式資料庫。

Figure 6.1: The monolithic architecture — single component, single repository, single deployment pipeline
巨石的優點#
- 單一程式碼庫的簡潔:大幅變更容易(可同時改 code 與 schema、build、deploy);IDE 與工具圍繞單一應用;編譯期 API 檢查——在 Java、Rust 這類編譯語言裡,介面誤用會被編譯器抓到,不需 contract testing。
- 系統操作易實作、易理解、易維護:操作天生在單一元件內,沒有跨網路通訊;可以單一 ACID 交易實作,自動具備 isolation 與 consistency。
- 部署相對單純:單一技術棧,典型部署是一組負載平衡的元件實例;但仍可做進階變化——例如 FTGO 的 Restaurant Management 子領域有大型記憶體資料庫,可分成兩個 cluster:多數請求跑在通用機器,Restaurant Management 跑在記憶體最佳化機器,共用同一資料庫(Spring Boot 可用 Spring Profiles 啟用對應功能)。

Figure 6.2: A monolithic application deployed as multiple clusters sharing the same database but optimized for different workloads
- 排錯較簡單:單一元件實例的請求,所有 log 都在一個檔案內;只有當操作橫跨多實例時,才需要 log aggregation 與 distributed tracing 等可觀測性模式。
ACID 例外:批次工作常刻意拆成多個 transaction,以改善錯誤處理、效能、可擴展性;有時也會把操作拆成同步部分(建立 Order + 發 event)與一或多個事件處理器(寄出確認信)兩段。巨石的關鍵優勢是「拆 transaction」是開發者自願的決定,而非被分散式架構強迫。
巨石如何滿足流暢交付的五個品質屬性#
小型團隊開發的小型巨石,通常能很好地滿足這五個品質屬性。問題出在大型巨石被大型組織開發時,巨石會逐漸成為流暢交付的阻礙。
可修改性:仰賴自律的設計#
- 巨石的領域視角完全可以做到鬆設計期耦合——這是設計問題,不是架構限制。元件視角只有一個元件,自然不會有元件層的耦合。
- 但巨石「太寬容」:可以隨便加類別到任何位置,程式都跑得動。時程壓力下開發者容易抄捷徑,逐漸累積成緊耦合,墮落為大泥球。
- 傳統用來組織領域視角的模式(分層架構等)在小巨石還行,但隨著規模成長就無法支撐——下面要介紹的「模組化巨石」是更有效的替代方案。
可演化性:隨應用與組織成長而下降#
大型巨石只有單一技術棧,升級需要全公司協調:時程衝突、利益不一致,連修補安全漏洞這類關鍵升級都可能拖延。
巨石架構也讓低成本實驗新技術變得困難——任何新技術都會影響整個應用,不是只影響一小部分。長期下來技術棧過時的風險極高。

Figure 6.3: Upgrading the technology stack of a monolith requires coordinating with all teams
可測試性:隨應用與組織成長而下降#
三個原因:
- 部署管線會變成瓶頸:整體測試套件執行時間隨應用變大而拉長,即使各子專案單獨快,加總起來仍慢;組織成長帶來變更量上升,讓管線塞車。
- 無法隔離非確定性模組:巨石不能把 flakey 的部分隔到自己的元件;flakey 測試讓開發者花時間判斷「這次失敗是真 bug 還是隨機」,白白燒掉時間。
- 本地測試變得不切實際:大型巨石需要超出筆電負荷的資源——只能買大筆電或雲端 IDE,但成本高昂,且即使最大型 EC2 也不一定跑得動。

Figure 6.4: Testability declines as the application and its organization grow — long test times, deployment pipeline becomes a bottleneck, flakey tests
可部署性:隨應用與組織成長而下降#
巨石可滿足以下四項可部署性:自動化部署、可配置不同環境、漸進式部署、可回滾;單一團隊巨石還能做到「一個團隊一個元件」。但其他三項會隨規模惡化:

Figure 6.5: A monolith satisfies four of eight deployability dimensions; ability to satisfy three others declines as it grows
- 部署速度下降:應用越大啟動越慢,canary release 還要啟動兩次;部署只能序列化,平行化幫不上忙。
- 可靠性下降:每次部署都更新整個應用,即便變更很小,潛在影響範圍卻很大,隱藏依賴與測試覆蓋空隙會放大事故風險。
- 團隊自主部署能力下降:所有團隊提交到同一個程式碼庫,任一團隊把 build 弄壞,其他人就無法部署。
- 驗證部署的時間拉長:canary release 用生產流量驗證新版,但巨石功能太多,信心需要更久才能建立——某些 bug 甚至可能直到幾天後 batch 跑時才被發現。
可觀測性:隨應用成長而下降#
可觀測性模式可實作,但 telemetry 都是「整個元件層級」的合計值;當巨石越大,個別子領域的行為越被遮蔽,排查越困難。
巨石的其他缺點#
- 無法支援多技術棧:Java 巨石只能用 JVM 兼容語言,即便其他語言或 runtime 在某些子領域更合適,也無法使用。
- 無法依特性隔離子領域:第 20 章會提到,子領域有不同的關鍵性、安全性等特徵;微服務可把它們部署成獨立服務以隔離,巨石做不到。
巨石的模式語言#
雖然巨石的有些限制(單一技術棧)無法消除,但下列模式可大幅提升「巨石對流暢交付的支援度」:
- Technology-Oriented Monolithic Architecture(技術導向):常見但有問題的結構,容易造成緊耦合與低模組化。
- Modular Monolith(模組化巨石):更好的結構,圍繞領域概念組織應用,提升可修改性、可測試性與可部署性。
- 一系列減少建置期耦合的支援模式。

Figure 6.6: The monolithic architecture pattern language — patterns to improve a monolith's support for fast flow
從技術導向到模組化巨石#
傳統分層巨石的問題#
以 FTGO 為例,各團隊負責的子領域(consumers、orders、notifications),程式碼被打散到 web/domain/persistence 三層,問題:
- 層的責任不清:無論應用多大,永遠只有三層,業務邏輯層的責任不斷膨脹。
- 緊設計期耦合:每個子領域跨層分布,加一個 Consumer 屬性就要改三層。
- 不對應團隊結構:團隊以子領域劃分,卻被迫穿層工作。
- 過度的建置期耦合:每層一個 Gradle 子專案,改一個類別會迫使其層與所有上層重建——小變更導致大量測試。

Figure 6.7: In a technology-oriented layered architecture, code for each subdomain is scattered across technical layers
模組化巨石#
模組化巨石以子領域為主軸組織應用,而非以技術概念分層。每個子領域對應一個「領域模組(domain module)」,是貫穿整個架構的垂直切片(包含 web、domain、persistence)。
範例 FTGO 結構:consumers/、orders/、deliveries/、main/(應用入口,依賴所有領域模組)。

Figure 6.8: A modular monolith consists of domain modules corresponding to the application's subdomains
「modular」不代表分層巨石「沒結構」——分層巨石也有結構(技術導向)。也許更貼切的名字是「領域導向架構(domain-oriented architecture)」。
即使是六角架構也常被稱為 domain-centric,但它仍以技術概念(adapter、port、domain)組織,並非以領域本身組織。
領域模組設計的四個關鍵決策#
- 內部領域架構
- 對外暴露的 API
- 資料庫 schema 組織方式
- 類別與套件如何組織為 build project
內部領域架構:用六角架構#
每個領域模組以六角架構組織:

Figure 6.9: A domain module has a hexagonal architecture
consumers.restapi:REST 入站介接卡(ConsumerController等)。consumers.domain:業務邏輯與 entity、repository 介面(Consumer、ConsumerRepository)。consumers.infrastructure:出站介接卡(例如ConsumerDao實作ConsumerRepository)。

Figure 6.10: A domain module consists of restapi, domain, and infrastructure sub-packages
API 設計:facade + DTO#
不要直接暴露 entity 與 repository 給其他模組——
Order模組能呼叫任何 public 方法,連「Consumer是 JPA entity」這個假設都會洩漏出去,且 entity/repository 難以 mock。

Figure 6.11: Order Domain Module directly invoking Consumer entity and ConsumerRepository — tight design-time coupling
更好的做法:facade 風格 API——例如 ConsumerService 介面定義 reserveCredit()、releaseCredit(),以 DTO 傳遞資料。Order 模組只依賴 facade,測試時可輕鬆 mock。

Figure 6.12: A ConsumerService facade encapsulates implementation details and reduces design-time coupling
資料庫設計:schema 視為私有實作#
- 領域模組的 schema 是私有實作細節;模組間協作必須透過 API。

Figure 6.13: Domain modules should collaborate through APIs rather than direct database access
- 例外:讓 SELECT 跨界有時是合理權衡——例如「查詢消費者訂單歷史」用 SQL JOIN 比兩次 API 呼叫高效得多。

Figure 6.14: Sometimes a query that spans multiple domain modules is implemented with a single SQL JOIN for efficiency
- 絕對避免跨界的 INSERT/UPDATE/DELETE(批次更新例外):會繞過或重複另一個模組的業務邏輯,引發資料一致性 bug。
跨模組系統操作的設計#
跨多個領域模組的操作(例如 createOrder() 跨 Order、Consumer、Notification)需要兩個關鍵決策:模組間如何協作、交易如何管理。
三種協作模式#

Figure 6.15: Three domain module collaboration patterns — Invoke, Observer, Asynchronous messaging
- Invoke 模式:直接方法呼叫——最簡單、最常用,搭配 transaction-per-operation 很合;缺點是設計期耦合與執行期耦合(呼叫方必須等被呼叫方回傳)。

Figure 6.16: The Invoke pattern — Order Module invokes reserveCredit() on Consumer Module
- Observer 模式:Order 在 Order 建立時通知,Notification 訂閱並反應。反向耦合:Notification 依賴 Order;讓 Order 容易遵循 Open-Closed 原則。但 observer 仍是同步呼叫,會被慢的 observer 拖累。

Figure 6.17: The Observer pattern — Order Management notifies Notification Management when an Order is created
- 非同步訊息(Asynchronous Messaging):透過 message channel(常用 message broker)交換訊息(多為 event)。能消除執行期耦合,但實作複雜——需要 atomic event publication、deduplication 等模式,且操作會被拆成多個 transaction。

Figure 6.18: createOrder() decoupled from notification delivery via an OrderCreated event on a message channel
createOrder()的合理設計:同步部分建立 Order 並發布OrderCreatedevent;非同步事件處理器負責呼叫外部 email 服務。這樣 email 服務 down 不會阻塞訂單建立,reliability、scalability 都更好。
三種交易管理策略#
- Transaction-per-operation:整個操作一個 ACID transaction(Spring 的
@Transactional即可宣告式設定)。這是巨石最大的優勢之一——除非有特殊需求,這是 Chris Richardson 推薦的預設做法。

Figure 6.19: The transaction-per-operation strategy — implement a system operation as a single ACID transaction
- Transaction-per-domain module:每個領域模組一個 transaction,需要 Saga(第 10 章);complicated saga 需要 compensating transaction,難實作、難理解,幾乎沒有任何巨石優勢卻得到微服務的麻煩。

Figure 6.20: createOrder() implemented as a Saga with three local transactions and a compensating transaction
- 例外:不需要 compensating 的簡單 saga 仍然 OK。例如
createOrder()第一個 transaction 跨 Order 與 Consumer(任何業務規則違反都在此發生),第二個跨 Notification。

Figure 6.21: A simpler saga for createOrder() that avoids compensating transactions
- 需求驅動的交易邊界(Requirements-driven):依操作需求而非架構元素設定邊界。
- 範例 1:批次工作刻意拆成多個 transaction 以改善錯誤處理、可擴展性、效能。
- 範例 2:呼叫外部系統前要先 commit 本地 transaction(因為外部系統通常無法加入交易),收到回應後可能再開新 transaction。
模組化巨石的測試策略#
- 測試金字塔:盡量把測試往下推。
- 領域模組獨立測試:減少變更牽動範圍。
- 應用級測試最少化:速度慢,只用來驗整體 wiring。
每個領域模組的測試套件:
- 單元測試:in-memory,測非平凡邏輯(例如演算法)。
- 整合測試:測介接卡(HTTP controller、DAO 等)。
- 模組測試:整個模組,搭配對被依賴模組的 test double;以 Spring Boot test 工具實作。
應用級測試:用 Test Containers 等框架建構並啟動應用容器映像,只驗證 @SpringBean 的 wiring,不重複模組內已測過的內容。
減少建置期耦合的物理設計#
John Lakos《Large Scale C++ Design》(1996)區分了邏輯設計(logical design)(命名、責任分配、繼承、封裝、鬆耦合、內聚)與物理設計(physical design)(原始檔、Gradle 專案/Maven 模組等)。物理設計直接影響建置期耦合,大型應用必須兩者並重。
三項物理設計技巧#
1. 為領域模組 API 拆出獨立 build project#
- 把領域模組拆成
consumer-api+consumer-impl。 - Client 只依賴穩定的
consumer-api,測試用介面 mock 即可。 - 改 implementation 不會觸發 client 的測試。

Figure 6.22: Reduce build-time coupling by splitting a domain module into an API and an implementation build project
2. 套用介面隔離原則(Interface Segregation Principle)#
- 即便有獨立 API project,未被使用的方法仍會造成耦合。
- 例:
ConsumerService.createConsumer()沒被 Order 使用,但若加新參數,Order 還是會被重測。 - 解法:把
ConsumerService拆為ConsumerService+CreditManagement兩個小介面,各自獨立 API project,共同依賴一個 common types project。 - Client 只與真正用到的 API project 建置耦合。

Figure 6.23: Apply the Interface Segregation Principle by splitting an API into smaller per-use-case API build projects
3. 切分 implementation build project#
- 同一 implementation project 內部也有建置期耦合:
web、persistence、domain都在同一個 project,改web也會重跑persistence的測試。 - 解法:
web與persistence各自獨立為 build project,都依賴domain但彼此不依賴。 - 慢測試(persistence 用實體資料庫)只在真正改動時才執行。

Figure 6.24: Split a domain module's implementation across multiple build projects (web/persistence/domain) to reduce build-time coupling
模組化巨石的限制#
仍然是巨石#
- 單一程式碼庫、單一技術棧、單一部署管線。
- 模組化能降低平均 build 時間,但無法降低最壞情況——廣泛使用的 API 或依賴一改,還是會觸發大量重建。
- 應用與組織持續成長後,可測試性、可部署性、可演化性、可觀測性的限制終究會浮現。
「模組化巨石可以平滑轉成微服務」是個迷思#
模組化巨石不一定是微服務的踏腳石,有兩個現實障礙:
- 領域模組之間的互動不適合網路通訊:模組內方法呼叫極快,但服務間網路通訊有 latency、執行期耦合與複雜度。天真地把方法呼叫換成服務呼叫,效能與可靠性會雙雙下降。
- 把單一交易拆成多個交易並不容易:巨石中常見的「跨模組單一 ACID 交易」在微服務中必須拆為多個 transaction,需要重新設計操作、引入 Saga(第 10 章),並處理一致性、併發、錯誤處理、回滾的全新複雜度。
章節重點摘要#
- 巨石架構是把應用結構為單一元件、單一程式碼庫、單一部署管線的簡單風格。
- 由於單一程式碼庫,開發較單純;大幅變更可一次提交完成;多數操作可以 ACID 交易實作。
- 小型應用、少數團隊是巨石的甜蜜點。
- 大型應用 + 多團隊會超出巨石承載力:單一管線變瓶頸,技術棧升級需大規模協調。
- 模組化巨石圍繞子領域組織程式碼,提升團隊自主性、降低建置期耦合。
- 仔細的 build project 設計能進一步加速部署管線——但無法消除最壞情況的 build 時間。
- 模組化巨石理論上可被設計為「微服務 ready」,但實務上模組互動方式與交易模型常讓抽取服務變得困難。
- 模組化巨石仍是巨石,某些應用與組織終究需要轉到微服務架構。