「我這個功能該寫成 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不能保證 atomicdb.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 知道全部 downstreamProducer 不知道誰訂閱
執行時序同步(立刻完成)非同步(背景處理)
用戶等待時間等全部做完立刻返回
Transaction可 atomic跨 handler 無法 atomic(要 Outbox)
DebugStack 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 的代價。