「我這個功能該寫成 Service.method() 直接呼叫,還是該 publish 一個 event 讓 handler 接?」
這個問題很多 backend 工程師問——通常聽到的答案是「看複雜度」、「事件驅動比較解耦」這種抽象的話,沒幫到忙。
這篇直接給判斷依據:MVC 同步呼叫跟 Event-driven 在做什麼根本不同的事、什麼場景該選哪個、為什麼大多數實際 codebase 是兩個都用。
同一個 task,兩種寫法的根本差異
舉例——「下單成功後要做這幾件事:寫資料庫、扣庫存、寄信、推 push、送 analytics、給推薦系統訊號」。
MVC 寫法:
OrderController.create()
├→ OrderService.create()
│ ├→ db.save(order)
│ ├→ InventoryService.deduct(item_id, qty)
│ ├→ EmailService.sendConfirmation(user)
│ ├→ PushService.notify(user)
│ ├→ AnalyticsService.track('order_created', order)
│ └→ RecommenderService.signal('purchase', user, item)
└→ return order
Event-driven 寫法:
OrderController.create()
├→ OrderService.create()
│ ├→ db.save(order)
│ └→ EventBus.publish('order.created', { order, user, items })
└→ return order
# 各 handler 獨立訂閱:
on 'order.created':
├→ InventoryHandler.deduct()
├→ EmailHandler.sendConfirmation()
├→ PushHandler.notify()
├→ AnalyticsHandler.track()
└→ RecommenderHandler.signal()
兩種寫法做同一件事——但架構決策完全不同。
4 個關鍵差異
差異 1:耦合方向
MVC:Controller / Service 必須知道全部 downstream。要新增「給推薦系統送訊號」就得回去改 OrderService。
Event-driven:Controller / Service 只 publish event,不知道誰在訂閱。新增 handler 不用動 producer——只要寫一個新 subscriber。
這是 event-driven 最被引用的好處——解耦。但解耦不是免費的(後面講)。
差異 2:同步 vs 非同步
MVC:所有呼叫同步發生。Controller return 之前,6 件事全部做完(或某件失敗整個 rollback)。
Event-driven:通常配合 message queue / event bus,handler 非同步執行。Controller publish event 後立刻 return,handler 在 background 跑。
這層差異有兩個 implication:
- 延遲:MVC 同步等所有事做完才回應 user;Event-driven 立刻回應,handler 慢慢跑
- 失敗模式:MVC 一件事失敗整個 fail;Event-driven 某 handler fail 不影響其他 handler,但 user 可能不知道有東西 fail 了
差異 3:Transaction 邊界
MVC:可以在同一個 DB transaction 裡做完——「db.save(order) + inventory.deduct()」一起 commit / rollback。consistency 強。
Event-driven:跨 handler 的 transaction不能保證 atomic。db.save(order) 已經 commit、event 已經 publish,但 EmailHandler 失敗——你的 DB 狀態跟 email 狀態不一致。
這個問題有解(Outbox pattern)但增加複雜度。MVC 不需要這層。
差異 4:Debug / Trace 難度
MVC:stack trace 直接看到「Controller → Service → 各個 sub-call」,debug 容易。
Event-driven:看不到 trace。Producer 發了 event 之後責任結束、handler 在另一個 process / thread / 甚至另一台機器跑。要看完整流程要靠 distributed tracing(OpenTelemetry / Jaeger)才能拼回來。
debug 難度通常低估。對小團隊是真實負擔。
一張對照表
| 維度 | MVC 同步 | Event-driven 非同步 |
|---|---|---|
| 耦合方向 | Producer 知道全部 downstream | Producer 不知道誰訂閱 |
| 執行時序 | 同步(立刻完成) | 非同步(背景處理) |
| 用戶等待時間 | 等全部做完 | 立刻返回 |
| Transaction | 可 atomic | 跨 handler 無法 atomic(要 Outbox) |
| Debug | Stack trace 直接看 | 需 distributed tracing |
| 新增 downstream | 改 Producer | 只加 subscriber |
| 失敗影響 | 一個失敗整個 fail | 某 handler 失敗不影響其他 |
| 適合的耦合度 | 緊耦合(核心交易流程) | 鬆耦合(旁支副作用) |
該用哪個——按場景而非按潮流
該用 MVC 的場景
1. 強一致性需求
跨 service 必須一起成功 / 一起 rollback——金流 / 庫存 / 訂單核心流程。db.save(order) + inventory.deduct() 失敗要一起 rollback——這個 case 用 event-driven 是自找麻煩。
2. User 等著回應
User 提交表單、要立刻知道結果——「下單成功了沒」「審核通過了沒」。用同步呼叫直接回應,不要讓 user 等 push notification 才知道。
3. 邏輯本來就緊耦合
「驗證 + 計算 + 儲存」這種有先後依賴的步驟,event-driven 會把簡單邏輯拆成 5 個 handler,可讀性大幅下降。
該用 Event-driven 的場景
1. 副作用 / 旁支邏輯
email 通知、push 推送、analytics 追蹤、推薦系統訊號——這些都是「主要業務完成後該發生但不在主流程責任內」的事。用 event-driven 把它們從主流程剝離。
2. 多個 downstream 訂閱
新增一個 service 要消費這個事件(例如新建 audit log service)——event-driven 不用動 producer,只要加 subscriber。
3. 流量削峰
主流程 burst 但 downstream 處理慢——把 event 丟 queue,downstream 慢慢消化。
4. 跨 module / service 解耦
不同 team 維護的 service 之間,用 event 解耦能避免互相耦合 deployment / migration 時程。
大多數 codebase 是「兩個都用」
實務上不是「選 MVC 或 Event-driven」二選一——是核心流程 MVC + 副作用 Event-driven:
OrderController.create()
├→ OrderService.create() # MVC 同步
│ ├→ db.save(order) # 強一致:跟訂單一起成功
│ ├→ InventoryService.deduct() # 強一致:庫存不能 over-sell
│ └→ EventBus.publish('order.created', ...) # 副作用走 event
└→ return order
# 副作用都 async:
on 'order.created':
├→ EmailHandler # 副作用,慢無妨
├→ PushHandler # 副作用
├→ AnalyticsHandler # 副作用
└→ RecommenderHandler # 副作用
這個混合模式涵蓋 80% 的真實系統。不要為了「architectural purity」把所有東西都改成 event-driven——核心交易跑 event-driven 是給自己挖坑。
跟微服務 Event-driven 的區別
這篇講的是應用內部 (application-level) 的 MVC vs Event-driven——同一個 process / monolith 裡用 in-memory event bus 還是 direct call。
跟「系統層 (system-level) event-driven 微服務架構」是不同層次的問題:
- 本篇 (application-level):單 monolith / service 內部的 module 之間怎麼溝通
- System-level event-driven:跨 service 用 Kafka / RabbitMQ 串起多個 microservice,例如 35-event-driven-basics
兩個概念有些重疊但 trade-off 點不同。本篇 focus 應用內部——broker 選型 / 微服務通訊那層見 B12 Queue 章節。
Anti-pattern:為了 event-driven 而 event-driven
社群有時候把 event-driven 當「進階 / 現代 / 高級」架構,把所有 controller → service 的呼叫都改成 publish event。這個做法問題很大:
- debug 變成噩夢:每個邏輯都得 distributed trace 才能看完整流程
- transaction 邊界錯亂:本該 atomic 的核心流程被拆成 event chain,consistency 不保
- 新人 onboarding 痛苦:理解一個 user click 觸發了什麼要看 5 個 handler
- 延遲變不可預測:簡單呼叫變成「發 event → handler 處理 → 可能 retry → 可能 DLQ」的 chain
event-driven 是工具不是哲學。哪邊有解耦 / 削峰 / fan-out 的 leverage 才用,沒有 leverage 的地方用了是負擔。
反思
MVC vs Event-driven 不是「新舊」的問題,是「這段邏輯該緊耦合還是鬆耦合」的判斷。
緊耦合的場景:核心交易、強一致性、user 等回應 → 同步 MVC。
鬆耦合的場景:副作用、多 downstream、流量削峰、跨 team 服務 → Event-driven。
兩者混用是常態,不是 anti-pattern。不要追求「全部 event-driven」——那是把工具當哲學,最後付出 debug 跟 consistency 的代價。