支付系統設計:訂單、支付、對帳的一致性與冪等
這篇文章是看了 s09g 在 YouTube 上發的「system design — payment system」(19:47)之後的整理筆記。
我必須先承認一件事:我在寫金流的時候,第一次看這種題目也是先翻 Stripe 文件、把 SDK 串起來、看到 payment_intent.succeeded 就覺得「啊我會接金流了」。但 s09g 影片一開始就潑了一盆冷水——你沒有金融牌照,你拿什麼去划帳?
金流系統設計的本質,不是「怎麼呼叫 PSP」,而是「怎麼把一筆支付的事實記下來,並且保證它永遠對」。這才是後端工程師真正要扛的事。
這篇文會順著影片的章節走:為什麼要透過 PSP、整合的 4 種方案、資料模型怎麼切、冪等怎麼做、狀態機怎麼設計、對帳、event sourcing、風控。每一段我都會加一點自己的觀察(也就是後端工程師半夜被叫醒時會想知道的那種)。
Table of Contents
為什麼不是「呼叫 PSP 就好」
很多人以為支付系統就是「前端收信用卡 → 打到 Stripe → 收 webhook → 訂單完成」。但如果你真的畫出這個流程圖,會發現你只畫了 1/10 的參與者。
完整的支付參與者至少有:持卡人(customer)、發卡行(issuer bank)、卡組織(Visa、Mastercard、JCB 這些 card network)、收單行(acquirer bank)、商戶(merchant)。光是把這些角色對齊就已經頭昏了——使用者可能拿著 30 家不同銀行的卡,商戶也對接 5 家收單行,如果你要一家一家去接,工作量跟合規負擔會讓你直接關掉筆電。
所以實務上商戶會找一個 PSP(Payment Service Platform),像 Stripe、Adyen、綠界、藍新。PSP 給你一組乾淨的 API,你呼叫就好,剩下的路由、轉發、跨行清算,他們處理。當然,他們會抽 0.5%–1% 的手續費,這是合理的代價。
系統邊界與責任分離
影片裡 s09g 強調一件事:你的瓶頸不在你自己這裡,在外部 PSP。因為你只是呼叫 API,吞吐量被外部限制住,所以 scalability 暫時不用列為非功能性需求。
這是一個很重要的思維翻轉——大多數後端面試題會要求你談 QPS、談 sharding、談 cache,但支付題目反過來:你先別想 scale,先想合規、可靠性、一致性、正確性。後面三個是金融業務的命根子。
責任分離長這樣:
- 商戶後端:發起支付請求、記帳、通知使用者
- PSP:路由、轉發、風控初篩、與銀行對接
- 銀行 / 卡組織:最終的扣款與清算
商戶負責「事實記錄」,PSP 負責「通路執行」,銀行負責「資金移動」。這個三段式的分工,後面所有的設計都是圍繞它展開的。
支付整合的 4 種方案
影片列了 4 種把使用者付費資料送到 PSP 的方式,我用台灣的脈絡重述並補充:
方案 1:Server-to-server(自建 PSP)
前端把卡號、CVV、到期日送到商戶後端,後端再轉給銀行或卡組織。
這條路等於你自己開一家 PSP。你需要拿到支付機構牌照、需要通過 PCI-DSS 完整稽核、需要保存敏感的 PAN(primary account number)。只有大型企業才會這樣做(像京東、淘寶早期、AWS 自己接的那種等級)。好處是通過 PCI 之後自主權大、能自訂分期分帳、可以省通道費。但初期投入與合規成本太高,一般新創絕對不要走這條路。
方案 2:JavaScript SDK + iframe + 客戶端加密(CSE)
這是 Stripe.js、TapPay、綠界 SDK 的標準做法。PSP 提供 JS library,商戶嵌入到自己的網站,付費時 SDK 把卡號直接送到 PSP(甚至不需要經過商戶伺服器),回傳一個不可逆的 token。商戶拿到 token 再去打 PSP 的扣款 API,永遠不接觸原始卡號。
但 SDK 仍然是跑在商戶頁面上的 JS,所以有兩個風險:
- XSS 注入:攻擊者在你的頁面注入腳本,攔截使用者輸入
- MageCart 攻擊:供應鏈攻擊,在第三方 SDK 裡偷卡號
所以前端必須做 CSP(Content Security Policy)、SRI(Sub-resource Integrity)、限制 script 來源、驗 hash。
進一步,現代 SDK 會在底層塞一個 iframe(hosted field),讓卡號輸入框實際上是 PSP 網域的子頁面,再用 PSP 的公鑰做 CSE(client-side encryption) 把卡號加密起來 post 出去。這樣即使商戶頁面被攻破,攻擊者也拿不到明文卡號。
方案 3:Hosted Redirect(託管頁面)
最簡單的方案:使用者點「結帳」→ 302 跳到 PayPal / Stripe Checkout / 綠界託管頁 → 付完跳回來。
PCI 負擔最低、運維最簡單、PSP 故障時全靠他們兜底。自由度也最低——UI 是別人的、品牌是別人的、轉換率會掉(多一次跳出)。台灣很多小店早期就是這樣接藍新、TapPay 的。
取捨總結
s09g 影片最後給了一句非常到位的結論:「可客製化越高,成本越高——不僅是技術成本,還有 PCI 責任和安全管控成本。」台灣的中小型電商我會建議從 方案 2 + iframe hosted field 開始,視需要再升到方案 1。千萬不要為了「接觸卡號比較帥」就直接走 server-to-server,那是把團隊推入合規深坑。
資料模型:三張表把責任切開
影片提到一個非常關鍵的設計:業務狀態、交易狀態、資金狀態,三種狀態應該放三張表。
原因很直接——支付資料是合規驅動,不可變更、必須可審計。如果你把「訂單狀態」跟「扣款狀態」混在同一張表,後端寫程式時會很容易不小心 UPDATE 錯欄位,導致審計人員追蹤時資料前後矛盾。
理論上至少要這三張表:
order table(業務狀態)
- 訂單 ID、商品、金額(業務幣別)、使用者、狀態(待付款 / 已付款 / 已取消 / 已退款)
payment table(交易狀態)
charge_id(PSP 端 ID)order_idpayment_method(信用卡 / ATM / LINE Pay)psp(Stripe / 綠界 / 藍新)status(state machine:created / start / processing / success / failed)amount、currencycreated_at、updated_atidempotency_key← 重點
ledger(資金狀態,append-only)
- 帳戶、借方金額、貸方金額、交易 ID、時間戳
- 永遠 append,不 UPDATE 不 DELETE
三張表讀寫模式完全不同:order table 是 CRUD;payment table 是「以 idempotency_key 為錨點的有限狀態機」;ledger 是純 append-only 的事件流。
ledger 為什麼是 append-only
金融審計的硬要求:「這筆帳寫入後就不能改」。如果你允許 UPDATE,理論上就有人可以掩蓋錯誤。所以 ledger 必須是 append-only。在 PostgreSQL 上你可以用觸發器或權限設定強制,在雲端可以用專用服務——AWS 有 QLDB(Quantum Ledger Database),阿里巴巴開源過 LedgerDB,都提供 SQL 接口但底層專為不可變特性優化。
雙式記帳
影片提到一個會計學名詞:double-entry bookkeeping(雙式記帳)。每筆交易至少影響兩個帳號、產生一組「金額相等、方向相反」的紀錄。借方 100、貸方 100,加總永遠是 0。這是會計幾百年來的鐵律——任何單邊記帳最終都會出錯。
實務上你可以在 ledger table 用兩筆 row 來表達一筆交易:
-- 持卡人帳戶 -100
INSERT INTO ledger (account_id, amount, txn_id, direction) VALUES ('user:42', 100, 'txn:abc', 'debit');
-- 商戶帳戶 +100
INSERT INTO ledger (account_id, amount, txn_id, direction) VALUES ('merchant:1', 100, 'txn:abc', 'credit');
只要加總為 0,內部就一定自洽。
冪等性:exactly-once 的真正意思
金流最怕的事:使用者被扣兩次錢。
這個事故怎麼發生的?想像一下:使用者在結帳頁面狂點按鈕(手機網路 lag、按鈕沒 disable、前端重試邏輯有 bug),結果伺服器收到 5 個相同的請求,每個都去 PSP 扣款一次,使用者崩潰、客服崩潰、法務也崩潰。
要避免這個,必須做到 exactly-once——同一筆邏輯交易,不管被打幾次,只會被處理一次。
idempotency key 的生命週期
s09g 的影片把這個流程講得很清楚:
- 使用者打開 checkout 頁面
- 伺服器生成一把 idempotency_key(例如 UUID),回傳給前端
- 前端把這個 key 放在後續所有 payment 請求裡
- 伺服器收到請求時,先去 payment table 用 key 查重
- 如果已經有 record,就回傳上次結果;如果沒有,就開始處理
這個 key 是 exactly-once 的核心。它讓「重試」變成安全操作,因為重試會拿到跟第一次相同的結果。
內部一致性 vs 外部一致性
影片把這兩個拆得很乾淨:
- 內部一致性:你的系統內部(例如 order 跟 payment 的狀態對齊)—— 用關聯式資料庫的交易就能搞定
- 外部一致性:你的系統跟 PSP 之間的狀態對齊—— 必須靠 idempotency
因為你不能預期 PSP 會乖乖地「只執行一次」。網路會抖動、PSP 會重試、你的 callback 會丟失。所以你的 idempotency key 必須穿透到 PSP,跟他們說:「這個 key 我已經處理過了,請不要再扣一次」。Stripe 文件裡的 Idempotency-Key header 就是這個意思。
狀態機與非同步通訊
PSP 是非同步的。你送出一個扣款請求,PSP 會回你「我收到了」(200 OK),但實際處理結果可能要幾秒甚至幾分鐘才會回。所以 payment 的狀態機至少要有:
created → start → processing → success
↓
failed
每個狀態轉換都要落 payment table,並且只能往「終態」前進,不能倒退。
retry 的四要素
當 PSP 沒回應時,你要重試。但不能無腦重試,必須有紀律:
- timeout:超過多久才重試
- exponential backoff:1s → 2s → 4s → 8s,每次翻倍
- jitter:每次加 ±15% 隨機抖動,避免雪崩——多個 client 同時重試時打散流量
- dead letter queue (DLQ):超過最大重試次數就丟進 DLQ,不要再撞牆
jitter 這件事 s09g 講得特別細:如果你只有 backoff 沒有 jitter,很多 client 會在同一時間點同步 retry,瞬間把 PSP 打死。jitter 是保護對方的設計,不是保護你自己。
callback + 輪詢
PSP 處理完會呼叫你給的 callback URL(webhook)。但 callback 會丟——網路會斷、你的 server 會重啟、防火牆會擋。所以 callback 不能當唯一的事實來源。
實務上兩條路並行:
- Webhook:PSP 主動通知
- Polling:超過 N 秒沒收到 callback,主動打 PSP 查詢
兩個都收到時,用 idempotency_key 去重,不會出錯。
狀態機跳變
還有一個討厭的 race condition:你的 server 已經 timeout 把狀態寫成 failed,結果 PSP 後來處理成功、callback 送過來說「其實是 success」。怎麼辦?
兩個解法:
- 單調狀態機:進入
failed後不再允許改成success - 樂觀鎖 / 版本號:每次更新帶
version,UPDATE 時檢查WHERE version = ?
最壞的情況,就是等日終對帳時由 settlement file 兜底。所以對帳才是金流的最終仲裁,不是 webhook、不是 polling。
對帳(Reconciliation)
為什麼還需要對帳?不是已經有 webhook + polling 了嗎?
因為 任何線上鏈路都可能遇到 bug。PSP 可能漏發 webhook、你的 server 可能丟訊息、網路可能 silent drop。一筆交易可能被記成 success 但其實銀行沒收到錢;也可能被記成 failed 但其實已經扣款。
這時候就要靠日終對帳:銀行(或 PSP)每天早上會送一份 settlement file 給你,列出昨天所有交易的最終狀態。你拿這份檔案跟自己的 payment table 比對——
- 在自己這邊成功、settlement 也成功:沒事
- 在自己這邊失敗、settlement 卻成功:使用者被扣款但訂單沒成立 → 自動退款或人工補單
- 在自己這邊成功、settlement 卻失敗:可能是 PSP bug → 人工審核
- 完全沒出現在 settlement:可能是交易被 PSP 風控擋下但忘了通知你
對帳出來的差異要觸發告警、自動補償、或轉人工審核。這是一條獨立的 batch job,不能跟線上即時流程混在一起,否則你會把 production 弄掛。
Event Sourcing 與風控
s09g 影片最後兩段是「加分題」:如果你還有時間,可以秀一下 event sourcing 跟 risk engine。
為什麼互聯網公司用 event sourcing
Stripe、Coinbase、Square 都用過 event store(Kafka / Pulsar) 作為 source of truth:
- 所有支付事件(payment.created / payment.captured / payment.refunded)都 append 到 event store
- 用 projection / view 從事件流計算出查詢用的 view(加速前端查詢)
- 任何糾錯都以「反向事件」或「補償事件」寫入,不會覆蓋歷史
這樣做的代價是系統複雜度高、開發與運維成本高、團隊要同時熟悉流系統與金融系統。但好處是:拿到金融正確性 + 互聯網可擴展性——可以重放事件、可以對帳時重新計算、可以接多個下游 consumer。
削峰填谷
如果把 MQ 放到最前面,Payment Service 變成 pure broadcaster,把每個事件投放到不同 DB:
- 一份進 payment table(線上查詢用)
- 一份進 ledger(金融記錄用)
- 一份進下游分析系統(BI 用)
每次對帳時,可以在 MQ 前端做事件重放,重新計算所有 view。這對偶發的資料錯誤非常有救。
Risk Engine
風控放在 Payment Service 前面。影片列出 4 種等級:
- 規則引擎:黑名單、白名單、規則樹——最簡單、合規友善、可解釋
- 統計 / ML:LR、XGBoost 之類的反詐欺模型——反應慢、需要標註資料
- 無監督學習:解決冷啟動問題——會有誤報、需人工介入
- 圖模型 / GNN:捕捉複雜詐騙網路——維運成本高、開發難
台灣中小型電商我會建議從規則引擎 + 統計模型起步,等到交易量到一定規模(單日 10 萬筆以上)再考慮上 GNN。風控模型最大的坑是誤報成本——擋下太多正常訂單會直接傷害營收,所以一開始寧可寬鬆再調嚴,也不要一開始就過度嚴格。
結語:給金流工程師的一句話
把整支影片看完,我最大的感觸是:金流系統設計的難度不在於「技術棧很新」,而在於「你的每一行程式碼都在碰真實的金錢」。
當你寫一般的後端服務時,bug 通常只是「使用者看到錯誤畫面」。當你寫支付系統時,bug 可能是「使用者被扣款但沒拿到商品」、「錢被轉到錯的帳戶」、「帳務報表對不上」——每一個都是會上新聞的等級。
所以金流工程師的核心素養,不是會串 SDK,而是對 合規邊界、資金一致性、非同步協作、可審計性 的直覺。
s09g 這支影片是個很好的起點,但他講得很保守(給面試用)。實際上線你還要對 PCI-DSS、PSP SLA、當地法規(台灣還有洗錢防制法、金管會的電子支付機構管理條例)。如果你是金流工程師,下一個值得挑戰自己的題目是:怎麼把對帳與金流觀測做成 dashboard,讓 ops 半夜被叫醒時不用翻 SQL。
金流這條路很硬,但相對地也比較少人走得深。值得走。
八、架構師怎麼看:金流系統在 2026 年的業界實作
影片從 PSP 整合、ledger 雙式記帳、冪等到 event sourcing 都講得很系統。但我有 4 個地方會做不同選擇,加上幾個影片沒講但生產上一定會遇到的細節。
1. 「不要自己做 PSP」的 trade-off 反轉
影片建議一律透過 PSP,這在 2018 年是對的。但 2024 之後 Stripe / Adyen 推出了「platform-of-platform」模式:
- Stripe Connect:你可以「成為下一層 PSP」,幫你平台上的小商家收款,分潤自動處理
- Stripe Issuing:你可以發行實體卡,繞過傳統發卡行流程
- Stripe Treasury:你可以開銀行帳戶給你的使用者(需美國 MSB 牌照)
這代表什麼?「自己接銀行」這條路徑的門檻其實降了。原本需要 PCI-DSS Level 1 + 完整稽核(數百萬 USD + 6-12 個月),現在透過 Stripe Issuing 可以在 2-4 週上線。
但這也有代價:
- Stripe 抽成更高(Issuing 部分通常 0.2% + 0.2 USD per transaction)
- 業務綁定:你發出去的卡只能在 Stripe 生態用
- 合規仍存在:Stripe Treasury 還是要 MSB 牌照,只是流程簡化
架構師判斷:
- 一般電商 / SaaS:用 PSP 就好(影片建議)
- 平台型業務(marketplace、creator economy):評估 Stripe Connect
- 金融科技 / 加密貨幣:直接走 Issuing + Treasury,不要跟傳統銀行打交道
2. QLDB 不是答案,PostgreSQL 觸發器就夠
影片提到 AWS QLDB / LedgerDB 是「不可變 ledger」的解法。我有不同看法:
QLDB 太貴:
- AWS QLDB pricing:$0.014 per 1K ledger writes,加上 storage + read cost
- 對一個中型支付系統(單月 1M 筆交易)來說,月費 $500-1500 USD
- 而且你要 lock-in 到 AWS 帳號
PostgreSQL + 觸發器 + revoke 權限就能達到同樣效果:
-- 強制 append-only
REVOKE UPDATE, DELETE ON ledger FROM PUBLIC;
-- 觸發器防止 truncate
CREATE OR REPLACE FUNCTION prevent_ledger_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Ledger is append-only';
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER ledger_immutable
BEFORE UPDATE OR DELETE OR TRUNCATE ON ledger
FOR EACH STATEMENT EXECUTE FUNCTION prevent_ledger_modification();
-- 定期 hash chain 驗證(防止有人繞過觸發器直接改檔案)
ALTER TABLE ledger ADD COLUMN row_hash CHAR(64);
ALTER TABLE ledger ADD COLUMN prev_hash CHAR(64);
業界現況:支付量 < 10M/月 用 PostgreSQL 觸發器就夠,> 100M/月 才需要考慮 QLDB 或自研的 event store。
我自己做過的支付系統,PostgreSQL 撐到單月 5M 筆完全沒問題。Partitioning + 觸發器的組合比 QLDB 便宜 90%。
3. Idempotency Key 的 window:24 小時是怎麼來的
影片沒講這個細節,但這是冪等設計的核心:
Stripe 內部 Idempotency Key 的保留時間是 24 小時,理由是:
- 業務場景:使用者刷卡後 24 小時內 retry 是合理的(網路問題、手機重啟、卡機問題)
- 24 小時後:retry 已經是新的 payment intent,使用者已經重新評估訂單
- 儲存成本:保留 24 小時的 key × 1KB × 100M 交易/月 = 100GB,可以接受
架構師實作建議(沿用我之前 POS 那篇提的 schema):
CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
payment_id UUID NOT NULL,
status VARCHAR(20), -- pending / completed / failed
response JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL -- created_at + 24h
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
-- 背景 job 每小時跑一次
DELETE FROM idempotency_keys WHERE expires_at < NOW();
Key 是要 expired 的,永久保留是浪費。但 24 小時是 Stripe 的內部數字,你的系統可以用 1 小時也可以用 7 天——重點是要有 expiry。
4. Event Sourcing 的真實成本:不是技術是組織
影片打槍「不要為了潮做 ES」,這個觀點在 2026 年其實要看團隊規模:
- 小團隊(< 5 人):ES 維運成本太高,Saga 就好
- 中型團隊(5-20 人):ES 開始划算,特別是有 BI / 風控 / 對帳三個需求時
- 大型團隊(> 20 人):ES 是標配,分工可以拆 stream team / core team / BI team
但 ES 真正的成本不是技術,是組織:
- 需要 stream-first 的工程師文化:新人不熟 Kafka、debug 事件流比 debug SQL 難 5 倍
- 需要 schema 治理:event schema 一改,所有下游 consumer 都要更新
- 需要 replay 工具:偶發事件錯誤要能 replay 才能修
架構師判斷邏輯:不是「Stripe 所以 ES」,也不是「小公司所以不用 ES」,而是「我們的工程師有沒有 stream-first 思維 + 我們的事件 schema governance 做得好不好」。前者靠培養,後者靠工具(Confluent Schema Registry / Apicurio)。
5. 對帳(Reconciliation)的現代化
影片講對帳是「日終 batch job 跑 settlement file 比對」,這個流程沒變,但實作方式可以現代化:
- 過去:shell script + cron + grep diff + email alert
- 現在:streaming reconciliation(Kafka Streams / Apache Flink)即時對帳,差異 < 1 分鐘就 alert
架構師現在會設計:
[PSP webhook] → [Kafka] → [streaming reconciliation]
↓
[對得上] → payment.status = confirmed
↓
[對不上] → Kafka topic: reconciliation.diff
↓
[alert + 自動補償 / 人工 review queue]
好處:差異不用等到隔天才發現,可能 1 分鐘內就抓到。
但這條路徑只有大流量(> 100K 筆/天)才划算。小流量用 shell script + cron 就好,過度工程反而是浪費。
6. Trade-off 反轉:Jitter 不一定要 Full Jitter
影片講 jitter 但沒給業界標準做法。AWS Architecture Blog 在 2015 年提出 "Exponential Backoff and Jitter" 論文後,業界有 3 種主流做法:
# 1. Full Jitter(AWS 推薦)
sleep_time = random.uniform(0, min(cap, base * 2 ** attempt))
# 2. Equal Jitter
sleep_time = (base * 2 ** attempt) / 2 + random.uniform(0, (base * 2 ** attempt) / 2)
# 3. Decorrelated Jitter
sleep_time = min(cap, random.uniform(base, prev_sleep_time * 3))
哪個最好?這要看場景:
- Full Jitter:平均延遲最低,但 variance 最高。適合「我希望 retry 越快完成越好」
- Equal Jitter:保留最低保證,適合「我有 deadline 不能太晚」
- Decorrelated Jitter:最均勻,但有極端值風險
業界實際:AWS SDK / Google Cloud SDK / Stripe SDK 全部用 Full Jitter——因為它在大多數情況下表現最好。Decorrelated Jitter 看起來 fancy,但 Netflix 自己後來也改回 Full Jitter。
架構師預設用 Full Jitter,除非有特殊 deadline 需求。
7. 風控模型的權重:規則 60% + ML 40%
影片列了 4 種風控等級,我的實務配比是:
- 規則引擎:60% 攔截率 — 黑名單、白名單、地區限制、金額上限、velocity check
- 統計 / ML 模型:30% 攔截率 — XGBoost / LightGBM 跑歷史 fraud pattern
- 無監督學習:5% 攔截率 — 冷啟動場景
- GNN:5% 攔截率 — 高價值帳號、VIP 帳號保護
理由:誤報成本遠比漏報成本高。一個正常使用者被擋下,他可能就不再來了;一個 fraud 漏報,最多損失一筆訂單金額。所以 ML 模型的 threshold 要偏嚴格,寧可讓 fraud 多過也不要誤殺正常使用者**。
8. 一個面試官會問的反問
如果面試官聽完你講 ledger 雙式記帳反問:「如果你的 ledger 寫到一半伺服器掛了,transactions table 已經 UPDATE 成 success,但 ledger 沒寫到,怎麼辦?」
正確答案不是「用分散式交易」(太重),也不是「這不會發生」(太天真),而是:
「Two-phase write with reconciliation:
- 先寫 ledger(status='pending')— 保證 ledger 一定有 record
- 然後 UPDATE transactions.status = 'success' — 業務狀態才推進
- 背景 worker 每分鐘掃 ledger WHERE status='pending' AND created_at < NOW() - 5min
- 對每筆 pending:查 transactions 表,如果已經 success 就 UPDATE ledger 為 success;如果還是 processing 就 alert + 人工介入
這就是為什麼對帳是金流的最終仲裁——不是 webhook、不是 polling、不是 idempotency,是 reconciliation。」
這個答案把「reconciliation 是仲裁機制」講出來了——面試官想聽的是這個設計哲學,不是「我會用分散式交易」這種沒經驗的答案。
架構師 takeaway:金流系統設計的難度不是「哪個技術比較潮」,而是「每個設計決定都有真實的金錢後果」。架構師的責任是「在合規與成本之間找到對的甜蜜點」——大部分團隊的甜蜜點是 PSP + PostgreSQL ledger + Saga + 24h idempotency window,不是 Event Sourcing,也不是 QLDB。Event Sourcing 是給「已經有 stream 維運能力、且業務量夠大」的公司準備的,不是給「覺得很潮所以想試」的人準備的。