Architecture Patterns
Interview Context
資深 DS / MLE 面試越來越常碰到架構題:你的 feature pipeline 要不要拆成獨立 service?實驗事件怎麼留 audit trail?分析讀模型跟交易庫怎麼分離?這頁整理面試最常出現的 architecture patterns — 重點不是背名詞,而是能講清楚每個 pattern 解決什麼問題、付出什麼代價、什麼時候不該用。
What You Should Understand
- 能比較 monolith、modular monolith、microservices 的 tradeoffs,並解釋為什麼「先從 modular monolith 開始」是主流建議
- 知道 microservices 的核心紀律:database per service、API contract、observability,以及 distributed monolith 這個反模式
- 能區分 event vs command、choreography vs orchestration,並用 saga 和 outbox pattern 處理分散式交易
- 理解 event sourcing 與 CRUD 的差異、CQRS 的讀寫分離,以及兩者搭配時的 eventual consistency
- 熟悉 DDD 關鍵術語(bounded context、aggregate、ubiquitous language)與 resilience patterns(circuit breaker、bulkhead、retry budget)
Monolith vs Modular Monolith vs Microservices
Three ways to structure a system:
- Monolith: single deployable unit, single codebase, usually single database. All modules call each other in-process.
- Modular Monolith: still one deployable unit, but the codebase is split into modules with explicit boundaries — each module owns its tables and exposes an internal API; other modules cannot reach into its internals.
- Microservices: each service is independently deployable, owns its own data store, and communicates over the network (REST/gRPC/events).
| Dimension | Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deploy unit | One artifact | One artifact | Many independent services |
| Team scaling | Poor — everyone touches everything | Good — teams own modules | Best — teams own services end-to-end |
| Operational complexity | Low | Low | High — network, discovery, tracing, deploys |
| Data ownership | Shared database | Module-owned tables, one physical DB | Database per service |
| Failure mode | Whole app down | Whole app down | Partial failure, cascading failure risk |
| Refactoring boundaries | Easy(in-process) | Easy | Hard — network API changes need coordination |
| Latency between components | Nanoseconds(function call) | Nanoseconds | Milliseconds(network hop) |
Start with a Modular Monolith
「先從 modular monolith 開始」的邏輯:
- 邊界一開始一定畫錯。系統早期你還不了解 domain,module 邊界會一直重畫。在 monolith 裡搬 code 是一次 refactor;在 microservices 裡搬邊界是跨 service 的 API migration + data migration,成本高一個數量級。
- Microservices 的成本是固定支出。service discovery、distributed tracing、CI/CD per service、on-call — 不管你有沒有 scale 問題都要付。
- Modular monolith 保留退路。如果 module 邊界乾淨(own tables、explicit interface),之後要把某個 module 拆成獨立 service,只需要把 in-process call 換成 network call。
When Microservices Pay Off
| Signal | Why It Justifies the Cost |
|---|---|
| Independent scaling needs | Inference service 需要 GPU + autoscale,CRUD API 不需要 |
| Team autonomy bottleneck | 多個 team 在同一個 codebase 上互相 block deploy |
| Different tech requirements | 一個 component 需要 Python + PyTorch,其他是 JVM |
| Independent release cadence | Model service 每天 deploy,billing service 每月一次 |
| Fault isolation | 推薦掛掉不能拖垮結帳流程 |
常見面試陷阱
被問「monolith 好還是 microservices 好」時,直接選邊站是弱回答。強回答是:這是 organizational 和 operational 的 tradeoff,不是技術優劣 — team 小、domain 不確定時 modular monolith 幾乎總是對的起點;microservices 是在 scaling(人或機器)出現明確瓶頸時才值得付的成本。
Microservices Best Practices
If you do go with microservices, these practices separate a working system from a distributed mess:
| Practice | What It Means | Why |
|---|---|---|
| Database per service | Each service owns its data store; others access via API only | 共用 DB 讓 schema 變成隱形耦合 — 改一張表要協調所有 service |
| Explicit API contracts | Versioned schemas(OpenAPI, protobuf), backward compatible changes | Consumer 不會因 provider 改欄位而在 runtime 爆炸 |
| Service discovery | Services find each other via registry / DNS, not hardcoded IPs | Instances 隨 autoscaling 動態增減 |
| Async where possible | Events for non-critical paths instead of sync call chains | 減少 latency 疊加與 cascading failure |
| Idempotent handlers | Same message processed twice gives same result | Message queue 是 at-least-once delivery,重複必然發生 |
| Independent CI/CD | Each service builds, tests, deploys alone | 一起 deploy 就失去拆分的意義 |
Observability: Logging, Metrics, Tracing
Distributed systems 的 debug 靠三根柱子:
| Pillar | Question It Answers | Tooling Examples |
|---|---|---|
| Logging | What happened in this service? | Structured JSON logs + centralized store(ELK) |
| Metrics | Is the system healthy overall?(rates, latency percentiles) | Prometheus + Grafana |
| Tracing | Where did this one request spend its time across services? | OpenTelemetry, Jaeger |
關鍵設計:每個 request 進入系統時產生一個 correlation ID(trace ID),跟著 request 穿過所有 service 和 message queue。沒有它,跨 service debug 幾乎不可能。
Anti-pattern: The Distributed Monolith
最糟的結果是 distributed monolith — 拆成很多 services,但保留了 monolith 的耦合:
- Services 必須一起 deploy(API 沒版本控制,改一個要全改)
- Services 共用同一個 database(schema 耦合)
- 一個 request 觸發長串同步呼叫鏈 A → B → C → D,任何一環掛掉全部失敗
- 結果:付了 microservices 的 operational cost(network、tracing、多套 deploy),卻沒拿到任何 independence 的好處
面試必考的反模式
Distributed monolith 是架構面試的高頻考點。判斷標準一句話:「如果你不能獨立 deploy 一個 service 而不協調其他 team,你就沒有 microservices — 你只有一個更難 debug 的 monolith。」
Event-Driven Architecture
In event-driven architecture (EDA), services communicate by publishing and consuming events instead of calling each other directly.
Events vs Commands
| Aspect | Event | Command |
|---|---|---|
| Semantics | Something happened(past tense fact) | Do something(imperative request) |
| Naming | OrderPlaced, ModelTrained | PlaceOrder, TrainModel |
| Audience | Zero or many subscribers — publisher doesn't know who | Exactly one handler |
| Coupling | Publisher 不依賴 consumer | Sender 依賴 receiver 存在且成功 |
| Failure expectation | Consumers 各自處理,publisher 不管 | Sender 通常在乎結果(成功/失敗) |
直覺:event 是「廣播事實」,command 是「指定某人做事」。用 event 的系統天生鬆耦合 — 新增一個 consumer(例如多接一個 analytics pipeline)不需要改 publisher。
Choreography vs Orchestration
跨多個 service 的流程有兩種協調方式:
| Aspect | Choreography | Orchestration |
|---|---|---|
| Control | 沒有中央控制 — 每個 service 聽 event 決定自己做什麼 | 一個 orchestrator 明確呼叫每一步 |
| Coupling | 最鬆 — services 只認識 event schema | Orchestrator 認識所有 participants |
| Visibility | 差 — 流程散落在各 service 的 event handlers | 好 — 流程集中一處,容易讀懂與監控 |
| Adding steps | 加 consumer 即可,不改別人 | 改 orchestrator |
| Debugging | 難(誰觸發了誰?) | 較易(orchestrator 有完整 state) |
| Best for | 簡單、步驟少、天然廣播型流程 | 長流程、需要補償邏輯、需要清楚狀態機 |
經驗法則:3 步以內、失敗處理簡單 → choreography;長流程、要處理各種失敗分支 → orchestration(用 workflow engine 例如 Temporal、Airflow 的角色類似)。
Saga Pattern: Distributed Transactions
Microservices 中沒有跨 service 的 ACID transaction(database per service)。Saga 把一個分散式交易拆成一串 local transactions,每一步都定義對應的 compensating action(補償動作):
- 每個 是某個 service 的 local transaction(自己 DB 內 ACID)
- 若第 步失敗,依序執行前面步驟的補償 (例如「扣款」的補償是「退款」)
- Saga 可以用 choreography(每個 service 聽前一步的 event)或 orchestration(saga orchestrator 統一調度)實作
注意:saga 給的不是 isolation — 中間狀態對外可見(例如訂單短暫顯示「已成立」後又取消)。設計時要接受 eventual consistency 並讓補償語意對業務合理。
Outbox Pattern
經典問題:service 要「寫 DB」+「發 event」兩件事,但這兩個動作跨了 database 和 message broker,無法包在同一個 transaction — 可能 DB 寫成功但 event 沒發出(或反過來),造成資料不一致。
Transactional outbox 的解法:
- 在同一個 DB transaction 裡,寫 business row 和 一筆 event 到
outboxtable(同一個 DB,所以 atomic) - 一個獨立的 relay process(polling 或 CDC 例如 Debezium)讀 outbox table,把 event 發到 broker,成功後標記已發送
- Relay 是 at-least-once — consumer 端必須 idempotent(用 event ID 去重)
這保證「state change 與 event 要嘛都發生、要嘛都不發生」,是 event-driven 系統的基礎建設。
Event Sourcing vs CRUD
Two Ways to Store State
- CRUD(state-as-snapshot): database 存「目前狀態」。update 就地覆寫,歷史預設消失。
- Event Sourcing(state-as-log): database 存「發生過的所有事件」的 append-only log。目前狀態不直接存,而是把事件從頭 replay 計算出來(可加 snapshot 加速)。
直覺:CRUD 像只記「帳戶餘額 = 320」;event sourcing 像銀行對帳單 — 記每筆存提款,餘額是加總出來的。對帳單永遠可以重算餘額,反之不行。
| Dimension | CRUD | Event Sourcing |
|---|---|---|
| What is stored | Current state(rows overwritten) | Immutable append-only event log |
| History / audit | Lost unless you add audit tables | Complete by construction |
| Read current state | Direct query(fast, simple) | Replay events or read projection |
| Time travel / replay | Impossible | Rebuild state as of any point |
| Debugging production issues | 只看得到結果 | 可以重播出 bug 發生的完整過程 |
| Schema evolution | Migrate tables | Must version events; old events live forever |
| Complexity | Low — every ORM does this | High — projections, snapshots, event versioning |
| Storage | Compact | Grows without bound(需要 snapshot / archiving) |
When Event Sourcing Is Worth It
- Audit 是硬需求:金融交易、醫療紀錄、實驗平台的 assignment 紀錄 — 「誰在什麼時候做了什麼」本身就是產品需求
- 需要 retroactive computation:新指標上線後,想對歷史資料重算(replay events 進新的 projection)
- Debug 與 reproduce:把 production 的事件流重播到 staging 重現 bug
When It Is Not
- 一般 CRUD app(user profile、設定頁):event sourcing 的複雜度(event versioning、projection lag、學習成本)遠超過收益
- Team 沒有 event-driven 經驗:這是全隊的 mental model 轉換,不是一個 library
和 ML 的連結
ML 系統其實早就在用類似概念:training data 的 immutable snapshot、feature 的 point-in-time correctness、experiment event log — 都是「不可變事實 + 由事實推導狀態」的思路。面試時能把 event sourcing 連到 feature store 的 point-in-time join,是很強的訊號。
CQRS
CQRS (Command Query Responsibility Segregation) separates the write model from the read model:
- Command side(write): 處理狀態變更,模型為了「正確性」設計 — normalized schema、business invariants、transaction
- Query side(read): 服務查詢,模型為了「讀取效率」設計 — denormalized views、預先 join、甚至不同的資料庫(例如 write 用 PostgreSQL,read 用 Elasticsearch)
兩邊透過 events 同步:write side 發出 change events,一個 projector 消費 events 更新 read model。
When CQRS Helps
| Situation | Why CQRS Fits |
|---|---|
| Read-heavy analytics views | Dashboard 查詢複雜(多表 join、聚合),直接打交易庫會拖垮 write path — 用 denormalized read model 預先算好 |
| Read/write scale asymmetry | 讀比寫多 100 倍 → read model 可以獨立 scale、加 cache、加 replica |
| Different query shapes | 同一份資料要支援 full-text search、時間序列、圖查詢 → 各自 project 到適合的 store |
| Pairing with event sourcing | Event log 本身難以查詢 — CQRS 的 projections 就是 event sourcing 的標配讀取層 |
Eventual Consistency Implications
Read model 是非同步更新的,所以有 replication lag:
- 使用者剛送出更新,馬上刷新頁面可能看到舊資料(read-your-own-writes 問題)
- 緩解手段:UI optimistic update、寫入後短暫改讀 write model、或在 response 帶 version 讓 read 端等到追上
- 面試重點:CQRS 不是 free lunch — 你用「同步一致性」換「讀寫各自最佳化」。如果業務不能接受 stale reads(例如扣款餘額檢查),該查詢就必須走 write model。
CQRS 不等於 Event Sourcing
兩者常一起出現但彼此獨立:你可以只做 CQRS(write DB + 物化出來的 read views,用 CDC 同步)而完全不做 event sourcing;也可以 event sourcing 而只有一個 read model。面試時混用這兩個詞是常見扣分點。
Domain-Driven Design Key Terms
DDD (Domain-Driven Design) 的核心主張:軟體的模組邊界應該跟著業務領域的邊界走,而不是技術分層。面試常考名詞定義與應用:
| Term | Meaning | Example |
|---|---|---|
| Bounded Context | 一個 model 有效的邊界 — 同一個詞在不同 context 意義不同,各自建模 | 「Model」在 Training context 是 artifact + hyperparameters;在 Serving context 是 endpoint + version |
| Ubiquitous Language | Team 與 domain experts 共用的精確詞彙,直接反映在 code 命名 | Code 裡叫 Experiment、Variant、Exposure,跟 PM 開會用同一套詞 |
| Aggregate | 一組必須一起維持 invariant 的 objects,有一個 root 作為唯一入口,transaction 邊界 | Order aggregate 包含 line items — 總金額 invariant 由 root 保證 |
| Entity | 有唯一 identity、生命週期中屬性會變的物件 | User(改 email 還是同一個 user) |
| Value Object | 沒有 identity、由屬性值定義、immutable | Money(100, "TWD")、DateRange — 值相等即相等 |
| Domain Event | Domain 中發生的、業務在乎的事實 | ExperimentStarted、ModelPromoted |
| Anti-corruption Layer | 在自己 context 與外部系統之間的翻譯層,避免外部 model 污染內部 | 把 legacy CRM 的欄位翻譯成自己 context 的概念再使用 |
DDD 與 microservices 的關係:bounded context 是最自然的 service 邊界。拆 service 拆錯的常見原因就是照技術分層拆(例如「API service」「DB service」),而不是照 domain 拆。
Clean Architecture and SOLID
The Dependency Rule
Clean Architecture organizes code in concentric layers. The rule: dependencies point inward only — inner layers never know about outer layers.
| Layer(inner → outer) | Contains | Knows About |
|---|---|---|
| Entities | Core business rules, domain objects | Nothing outside itself |
| Use Cases | Application-specific workflows | Entities |
| Interface Adapters | Controllers, presenters, gateways | Use cases |
| Frameworks & Drivers | Web framework, DB, external APIs | Adapters |
直覺:業務邏輯是最穩定、最值錢的部分,不該依賴最常換的東西(framework、DB)。內層透過 interface(port) 定義它需要什麼,外層提供實作(adapter)— 這就是 dependency inversion。
SOLID in One Line Each
| Principle | One-liner |
|---|---|
| S — Single Responsibility | 一個 class 只有一個改變的理由 |
| O — Open/Closed | 對擴充開放,對修改封閉 — 加功能用新增,不用改舊 code |
| L — Liskov Substitution | 子類別可以無痛替換父類別,不改變正確性 |
| I — Interface Segregation | 多個小 interface 勝過一個肥 interface — client 不該被迫依賴用不到的方法 |
| D — Dependency Inversion | 依賴抽象而非實作 — 高層模組不依賴低層細節 |
Why This Matters for ML Code
ML codebase 最常見的病:training script 直接 import BigQuery client、硬編 GCS path、model 邏輯跟 orchestrator 綁死。套用 dependency rule 的實際好處:
- 可測試:
FeatureRepository是 interface → unit test 塞 in-memory fake,不用連 warehouse - 可搬移:換 data warehouse / serving framework 只改 adapter,core pipeline 不動
- 可重用:同一份 feature transform 邏輯,batch(Spark adapter)和 online(service adapter)共用 — 這正是消除 train-serving skew 的結構性解法
Resilience Patterns
Distributed systems 的元件一定會壞 — resilience patterns 的目標是「局部失敗不變成整體失敗」。
| Pattern | Problem It Solves | How It Works |
|---|---|---|
| Timeout | 慢的依賴把你的 threads 全部卡死 | 每個外部呼叫都設上限;沒有 timeout 的呼叫是定時炸彈 |
| Retry with backoff + jitter | 暫時性錯誤(network blip) | 重試 + exponential backoff + 隨機 jitter,避免同時重試造成 thundering herd |
| Retry budget | Retry 本身放大流量、壓垮已經在掙扎的下游 | 限制 retry 佔總流量的比例(例如 10%);超過就直接失敗 |
| Circuit breaker | 對已經掛掉的依賴持續打流量,拖慢自己也妨礙對方恢復 | 錯誤率超過門檻 → open(直接 fail fast)→ 一段時間後 half-open 放少量流量試探 → 成功則 close |
| Bulkhead | 一個依賴的故障耗盡共用資源(thread pool、connection pool) | 每個依賴用隔離的資源池 — 像船艙隔板,一艙進水不沉全船 |
| Graceful degradation | 依賴完全不可用時,全功能失敗 | 準備 fallback:推薦服務掛了 → 回傳熱門榜;feature store timeout → 用 default feature values |
Circuit Breaker States
ML Serving 的 Degradation 階梯
面試講 ML serving resilience 時,準備一個 fallback 階梯會非常加分:full model → cached predictions → simpler/smaller model → popularity baseline → static default。每往下一階犧牲一點品質換可用性。重點:fallback 要在平時就演練,且 monitoring 要能看到「現在有多少流量走在 fallback 上」。
Real-World Use Cases
Case 1: ML Platform — 從 Monolith 拆出 Feature 與 Serving Service
你們的 ML platform 一開始是一個 Django monolith:資料標註、feature 計算、training 觸發、模型 serving 全在同一個 codebase、同一個 PostgreSQL。隨著模型數量成長,出現兩個明確訊號:(1)serving 的流量是其他功能的 200 倍,每次全站 deploy 都造成 inference 短暫抖動;(2)serving 需要 GPU node 和獨立 autoscaling,但整個 monolith 只能一起 scale。
套用本頁概念的決策過程:
- 先確認 monolith 內部是否已經 modular — serving module 是否只透過明確 interface 拿 feature?如果不是,先在 monolith 內把邊界整理乾淨(modular monolith),否則拆出去就是 distributed monolith
- 依 bounded context 拆:serving context(低延遲、高可用)與 feature context(batch 計算、正確性優先)的需求本質不同,是自然的 service 邊界
- 拆出後遵守 database per service:serving 用自己的 online store(Redis),不再直接讀 platform 的 PostgreSQL
- 為 serving 加上 timeout + circuit breaker + fallback(feature 拿不到就用 default values),避免 feature store 抖動打掛 inference
Interview follow-ups:
- 你怎麼判斷「現在」是拆的時機,而不是再等等?(獨立 scaling / deploy cadence / fault isolation 三個訊號)
- 拆出去之後,training 和 serving 的 feature 邏輯怎麼保持一致?(shared transform library 或 feature store — 連回 train-serving skew)
- 如果拆完發現兩個 service 每次都要一起 deploy,代表什麼?(邊界畫錯,distributed monolith 警訊)
Case 2: 實驗平台 — 用 Event Sourcing 做 Assignment 的 Audit Trail
你負責公司的 A/B testing 平台。法務與 data governance 要求:任何時間點都要能回答「user X 在時間 T 屬於哪個 variant、是被哪條規則分進去的」。原本的 CRUD 設計只存「user 目前的 assignment」,一旦 experiment 改配置或 rollback,歷史就被覆寫 — 事後根本無法重建。
改用 event sourcing 的設計:
- 把每個動作記成 immutable event:ExperimentCreated、TrafficAllocationChanged、UserAssigned、UserExcluded、ExperimentStopped
- 「user 目前的 assignment」變成 replay events 得出的 projection,另外物化一份供低延遲查詢
- Audit 需求 by construction 滿足:replay 到時間 T 就能重建當時的完整狀態
- 額外紅利:發現 metrics 異常時,可以把 production 的事件流 replay 到 staging,重現 assignment bug 發生的過程;新的分析需求(例如重算 exposure 定義)直接對歷史 events 跑新 projection
Interview follow-ups:
- Event log 無限成長怎麼辦?(snapshot + 冷資料 archive;replay 從最近 snapshot 開始)
- Event schema 要改版怎麼處理?(events immutable — 用 versioned event types + upcaster,舊事件永遠要能讀)
- 為什麼不乾脆在 CRUD 上加一張 audit table 就好?(audit table 是事後補記、容易漏寫且與 state 可能不一致;event sourcing 中 events 就是 source of truth,不可能不一致)
Case 3: 交易系統 — 用 CQRS 分離交易庫與分析讀模型
你是電商公司的 DS,BI dashboard 和你的 churn model 的 feature query 都直接打訂單交易庫(normalized PostgreSQL)。旺季時分析查詢的大 join 把交易庫 CPU 吃滿,checkout latency 飆高 — 分析工作負載正在傷害賺錢的 write path。
CQRS 式的重構:
- Write model 不動:訂單服務繼續用 normalized schema 保證交易正確性
- 訂單服務透過 outbox pattern 可靠地發出 OrderPlaced、OrderRefunded 等 events(避免「DB 寫了但 event 沒發」的不一致)
- Projector 消費 events,維護 denormalized read models:BI 用的寬表(訂單 + 用戶 + 商品預先 join)、你的 feature 用的每日聚合表
- 分析與 feature 查詢全部改打 read model — 交易庫只服務交易
- 接受 eventual consistency:read model 落後秒級。對 BI 和 daily feature 完全無感;但「下單後即時防詐檢查」這種不能容忍 stale 的查詢,仍然走 write path
Interview follow-ups:
- Read model 壞掉或 projector 有 bug 怎麼辦?(read model 是 derived data — 修好 projector 後從 event log / CDC 重建即可,這是 CQRS 的大優點)
- 怎麼監控 read model 的 lag?(consumer lag metric + 資料端的 freshness check,lag 超標要告警)
- 這個架構跟直接用 read replica 差在哪?(replica 是同 schema 的複本,只解流量不解 query shape;CQRS 的 read model 可以是完全不同的 denormalized 結構甚至不同 DB)
Hands-on: Architecture Patterns in Python
Outbox Pattern: Atomic Write of State and Event
import json
import uuid
import sqlite3
def place_order(conn: sqlite3.Connection, user_id: str, amount: float):
"""Write the business row AND the outbox event in ONE transaction."""
order_id = str(uuid.uuid4())
event = {
"event_id": str(uuid.uuid4()),
"type": "OrderPlaced",
"payload": {"order_id": order_id, "user_id": user_id, "amount": amount},
}
with conn: # single transaction: both rows commit or neither does
conn.execute(
"INSERT INTO orders (id, user_id, amount, status) VALUES (?, ?, ?, ?)",
(order_id, user_id, amount, "PLACED"),
)
conn.execute(
"INSERT INTO outbox (event_id, type, payload, published) VALUES (?, ?, ?, 0)",
(event["event_id"], event["type"], json.dumps(event["payload"])),
)
return order_id
def relay_outbox(conn: sqlite3.Connection, broker):
"""Separate relay process: poll unpublished events, push to broker."""
rows = conn.execute(
"SELECT event_id, type, payload FROM outbox WHERE published = 0 ORDER BY rowid"
).fetchall()
for event_id, event_type, payload in rows:
broker.publish(event_type, payload) # at-least-once delivery
conn.execute("UPDATE outbox SET published = 1 WHERE event_id = ?", (event_id,))
conn.commit()
# consumers must deduplicate by event_id (idempotency)
Event Sourcing: Apply and Replay
from dataclasses import dataclass, field
@dataclass
class Account:
"""State is DERIVED from events, never stored directly."""
account_id: str
balance: float = 0.0
events: list = field(default_factory=list) # append-only log
def apply(self, event: dict):
"""Pure state transition — no side effects, deterministic."""
if event["type"] == "Deposited":
self.balance += event["amount"]
elif event["type"] == "Withdrawn":
self.balance -= event["amount"]
def deposit(self, amount: float):
event = {"type": "Deposited", "amount": amount}
self.events.append(event) # record the fact first
self.apply(event) # then update in-memory state
def withdraw(self, amount: float):
if amount > self.balance: # invariant checked by the aggregate root
raise ValueError("insufficient funds")
event = {"type": "Withdrawn", "amount": amount}
self.events.append(event)
self.apply(event)
@classmethod
def replay(cls, account_id: str, events: list) -> "Account":
"""Rebuild current state from the full event history."""
account = cls(account_id)
for event in events:
account.apply(event)
account.events = list(events)
return account
# replay gives time travel: rebuild state as of any prefix of the log
# history = load_events("acct-1")
# state_now = Account.replay("acct-1", history)
# state_then = Account.replay("acct-1", history[:10])
CQRS: Projecting Events into a Read Model
from collections import defaultdict
class OrderStatsProjection:
"""Read model optimized for analytics queries.
Updated asynchronously by consuming write-side events."""
def __init__(self):
self.revenue_by_user = defaultdict(float) # denormalized view 1
self.order_count_by_user = defaultdict(int) # denormalized view 2
self.seen_event_ids = set() # idempotency guard
def handle(self, event: dict):
if event["event_id"] in self.seen_event_ids:
return # duplicate delivery — at-least-once broker
self.seen_event_ids.add(event["event_id"])
if event["type"] == "OrderPlaced":
self.revenue_by_user[event["user_id"]] += event["amount"]
self.order_count_by_user[event["user_id"]] += 1
elif event["type"] == "OrderRefunded":
self.revenue_by_user[event["user_id"]] -= event["amount"]
def top_spenders(self, k: int = 10):
"""Query answered from the read model — write DB never touched."""
ranked = sorted(self.revenue_by_user.items(), key=lambda x: -x[1])
return ranked[:k]
# rebuild is cheap: drop the projection, replay all events through handle()
Interview Signals
What interviewers listen for:
- 你把 monolith vs microservices 講成 organizational/operational tradeoff,而不是技術優劣,並主動提出「先 modular monolith」的演進路線
- 你能點名 distributed monolith 的判斷標準:不能獨立 deploy、共用 DB、長同步呼叫鏈
- 你分得清 event vs command、choreography vs orchestration,並知道 outbox + idempotent consumer 是 event-driven 的可靠性基礎
- 你不會把 CQRS 和 event sourcing 混為一談,且能講清楚 eventual consistency 對產品的實際影響(read-your-own-writes)
- 你能把架構概念連回 ML 場景:bounded context 對應 training/serving 拆分、dependency inversion 消除 train-serving skew、degradation 階梯保護 inference
Practice
Flashcards
Flashcards (1/10)
為什麼主流建議是「先從 modular monolith 開始」而不是直接上 microservices?
(1)早期 domain 不熟,module 邊界一定會重畫 — monolith 內搬 code 是一次 refactor,跨 service 搬是昂貴的 API + data migration。(2)Microservices 的 operational cost(discovery、tracing、多套 CI/CD)是固定支出。(3)Modular monolith 邊界乾淨的話,之後拆 service 只是把 in-process call 換成 network call。
Quiz
團隊 5 人、產品剛起步、domain 還在快速變動。最合理的架構起點是?