<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Event-Contract on Tarragon</title><link>https://tarrragon.github.io/blog/tags/event-contract/</link><description>Recent content in Event-Contract on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 11 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/event-contract/index.xml" rel="self" type="application/rss+xml"/><item><title>3.7 Event Contract 與 Replay Boundary</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</guid><description>&lt;p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。&lt;/p>
&lt;h2 id="event-contract">Event Contract&lt;/h2>
&lt;p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。&lt;/p>
&lt;p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。&lt;/p>
&lt;p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 或 partition 設計才有可依附的語意。&lt;/p>
&lt;h2 id="schema-compatibility">Schema Compatibility&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility&lt;/a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。&lt;/p>
&lt;p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。&lt;/p>
&lt;h2 id="replay-boundary">Replay Boundary&lt;/h2>
&lt;p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。&lt;/p>
&lt;p>replay window 要和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 對齊，讓事後能回放當時重播的是哪一批事件。&lt;/p>
&lt;h2 id="compensation">Compensation&lt;/h2>
&lt;p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。&lt;/p>
&lt;p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log&lt;/a>。&lt;/p>
&lt;h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映&lt;/h2>
&lt;p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration&lt;/a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。&lt;/p>
&lt;p>&lt;strong>典型概念對映差異&lt;/strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：&lt;/p></description><content:encoded><![CDATA[<p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。</p>
<h2 id="event-contract">Event Contract</h2>
<p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。</p>
<p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。</p>
<p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 或 partition 設計才有可依附的語意。</p>
<h2 id="schema-compatibility">Schema Compatibility</h2>
<p><a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility</a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。</p>
<p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。</p>
<h2 id="replay-boundary">Replay Boundary</h2>
<p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。</p>
<p>replay window 要和 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 與 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 對齊，讓事後能回放當時重播的是哪一批事件。</p>
<h2 id="compensation">Compensation</h2>
<p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。</p>
<p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映</h2>
<p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration</a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。</p>
<p><strong>典型概念對映差異</strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：</p>
<ul>
<li><strong>Partition (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka partition 是物理分片 + 順序邊界；Pub/Sub subscription 是邏輯 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、無物理分片概念。靠 Kafka partition 保證 per-key 順序的 consumer、遷到 Pub/Sub 改用 ordering key</li>
<li><strong>Offset (Kafka) 對映成 message attribute (Pub/Sub)</strong>：9.C9 case 原文對映方向；replay 模型差異上、Kafka offset 是位置指標、可任意回放到某個 offset；Pub/Sub 用 Snapshot + Seek API 達成類似 replay 能力、模型不同</li>
<li><strong>Consumer Group (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka consumer group 內部 rebalance 自動分 partition；Pub/Sub subscription 自動分 message、語意接近但 rebalance 細節差異會影響 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> message 處理順序</li>
</ul>
<p><strong>遷移評估要驗證的業務語意</strong>：</p>
<ul>
<li>順序保證：原系統靠 partition / consumer group 保證什麼順序、新系統能否複製</li>
<li>Replay 模型：原系統 replay 方式、新系統的 replay 工具能否達成同範圍。replay window 上限由 idempotency 保留期反推、見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Replay 跟 Idempotency 共設計</a></li>
<li>失敗模式：consumer 故障時、原系統的 rebalance / redelivery 行為、新系統會不會差異</li>
</ul>
<p>判讀重點：broker migration 屬語意對映工程、吞吐能力比較是次要驗證項。對應 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 的「Broker 遷移階段流程」</a>、實作面用 dual-write + shadow consume + cutover、驗證面靠 event contract 跟 replay 邊界做對帳。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 03 內部：replay window 跟 idempotency 共設計回到 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>；broker 遷移階段流程回到 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a></li>
<li>與 04 的交接：event contract 演進 + replay 邊界進 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>與 06 的交接：event contract 跟 replay 驗證進 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 跟 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li>與 07 的交接：event payload 的 PII / audit 邊界進 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data protection and masking</a></li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>Broker 選型前要先回答：</p>
<ol>
<li>event contract 是否能支援版本相容。</li>
<li>consumer 是否能用 dedup key 判斷重複。</li>
<li>replay window 是否能用查詢與指標證明。</li>
<li>不可逆副作用是否有補償流程。</li>
<li>event payload 是否包含 PII 或 audit-sensitive 欄位。</li>
</ol>
<p>這些問題決定後續要比較 broker retention、schema registry、DLQ、partition 與 replay 工具，並把吞吐放回服務語意下判讀。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體 broker 文章要承接本篇的 event contract 與 replay boundary。Kafka 的長期 retention、RabbitMQ 的 routing 與 DLQ、SQS 的 <a href="/blog/backend/knowledge-cards/visibility-timeout/" data-link-title="Visibility Timeout" data-link-desc="說明訊息被取走後對其他 consumer 暫時不可見的時間窗，timeout 後重新投遞">visibility timeout</a>、NATS JetStream 的 stream/consumer 模型，都要放回事件契約與重播邊界下判讀。</p>
<p>若事件需要長期 replay，後續文章要比較 retention、offset、partition 與 schema evolution。若事件主要是工作任務，後續文章要比較 visibility、ack、DLQ 與重試治理。若事件包含 PII 或高風險副作用，後續文章要比較 audit、encryption、access control 與補償流程。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 outbox 與事件發布一致性，接著讀 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 與發佈一致性</a>。要處理 consumer 端去重與重播，接著讀 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計與去重</a>。</p>
]]></content:encoded></item></channel></rss>