# Replay-Safe Architecture: Building Event-Driven Systems That Can Recover With Confidence

Ishan Shah · PayPal · WeAreDevelopers World Congress NA 2026

> Replay an toàn cho Kafka pipeline: idempotency hai lớp Redis và DynamoDB, recovery pipeline tách riêng, và runbook Scope, Isolate, Execute, Prove.

Topics: Distributed Systems

Canonical: https://homus.dev/talks/replay-safe-architecture-building-event-driven-systems-that-can-recover-with-confidence

## 1. Mở đầu: một production incident

Speaker là Ishan Shah, Software Engineer tại PayPal. Chủ đề: xây dựng event-driven system có thể recover một cách tự tin.

Anh mở đầu bằng thứ thường làm mọi người trong ngành tỉnh ngủ ngay lập tức: một production incident.

![Slide tiêu đề Replay-Safe Architecture, Ishan Shah đứng trên sân khấu Stage 3](https://homus.dev/photos/IMG_3918.JPG)

Slide mở đầu: "Replay-Safe Architecture. Building event-driven systems that can recover with confidence", Ishan Shah, Software Engineer, PayPal. Ishan đứng giữa sân khấu Stage 3 trước khi vào phần incident.

Câu chuyện của talk xoay quanh việc xây một **inventory pipeline**, rút từ kinh nghiệm của anh ở một công ty retail trước đây, nơi anh từng xây hệ thống marketplace inventory và fulfillment ([theo phỏng vấn của anh với HackerNoon](https://hackernoon.com/ishan-shah-on-recoverable-systems-ai-guardrails-and-the-internets-useful-weirdness), công ty đó là Nordstrom). Nhưng anh nhấn mạnh logic này áp dụng được cho bất kỳ event-driven microservice nào. Câu hỏi trung tâm là: chúng ta làm gì khi mọi thứ hỏng?

Trong một event-driven system lý tưởng, thứ chúng ta quan tâm thường là: pipeline có đang xử lý bình thường không, có lag không, sau một sự cố infrastructure thì có catch up lại được không. Nhưng có một sắc thái nhỏ mà gần như không ai để ý: **trong lúc infrastructure hỏng thì chuyện gì đã xảy ra?** Có business event nào bị bỏ lỡ không? Thứ này thường không bị phát hiện ngay. Nó lộ ra muộn hơn, qua một đợt reconciliation hay một đợt audit. Và lúc đó, trong khi production vẫn đang xử lý bình thường dữ liệu hiện tại, ta phải quay ngược lại lịch sử để sửa một thứ gì đó, **trong lúc production vẫn chạy**. Talk này nói về đúng việc đó: xây một kiến trúc recovery, hay kiến trúc retryable, cho event-driven system.

## 2. Infrastructure đã hồi phục, nhưng data incident thì chưa

Ishan đưa ra pipeline mà nhóm anh đã có, cũng là nơi bài học này xuất phát. Với Kafka broker, chuyện node cần được thay mới (replenish) hay broker bị restart là rất bình thường. Nhưng chính những gián đoạn mạng "bình thường" đó có thể khiến **hàng triệu event không được publish ra ngoài**, vì broker không sẵn sàng đúng lúc.

![Slide 01 Incident: timeline broker event, pipeline recovers, traffic moves on, PI disagrees](https://homus.dev/photos/IMG_3919.JPG)

Slide "01 · Incident": "The infrastructure incident was over. The data incident wasn't." Một timeline bốn điểm từ trái sang phải: broker event (bảo trì broker hoặc di chuyển cluster), pipeline recovers, traffic moves on, và cuối cùng là PI disagrees (đợt kiểm kho vật lý cho ra con số khác). Khung dưới: "Infrastructure can recover long before business state does. The failure that matters may survive after every operational alert clears." Nghĩa là mọi alert vận hành có thể đã xanh hết mà lỗi thật sự quan trọng vẫn còn đó.

Trong một pipeline lý tưởng, ta thấy broker sập, pipeline dần hồi phục, infrastructure đã ổn, traffic tiếp tục chạy. Nhưng trong khoảng thời gian infrastructure sập thì chuyện gì đã xảy ra với dữ liệu? Trong thế giới retail và e-commerce, thứ này thường bị bắt ở một khâu gọi là **PI, physical inventory** (kiểm kê hàng tồn vật lý). Đây là một trong những phần gây đau đầu nhất cho business, vì họ phải đóng cửa hàng theo đúng nghĩa đen, bỏ ra cả một ngày để đếm xem thực sự có gì trên sàn.

Và thế là, như Ishan nói, cuộc đối đầu thật sự là: **event-driven system của chúng ta so với một người cầm clipboard đi đếm hàng**. Trong một số tình huống, cái clipboard thắng. Đó chính là thứ nhóm anh muốn sửa. Việc xử lý đã diễn ra bình thường không có nghĩa là lịch sử không còn quan trọng. Ta cần quay lại sửa lịch sử đó và phục hồi, để business state khớp với trạng thái mong đợi.

## 3. Pipeline CDC tới Kafka, và cái hộp màu cam ở cuối

Ishan mô tả event-driven system mà nhóm anh vận hành. Mọi thay đổi business xảy ra trên Oracle được bắt bằng **change data capture (CDC)**, có thể là [Oracle GoldenGate](https://www.oracle.com/integration/goldengate/) cho Oracle hoặc [Debezium](https://debezium.io/), rồi đi vào một Kafka topic. Sau đó consumer xử lý và publish tiếp xuống các downstream service cần biết về các event đó.

Thay đổi trên Oracle đi qua CDC vào Kafka topic, consumer xử lý rồi publish xuống downstream. Hộp màu cam ở cuối (downstream service) là nơi hỏng: consumer đã consume và xử lý xong nhưng không publish được, trong khi Kafka đã đi tiếp.

Nhưng nhìn vào cái hộp màu cam ở cuối, tức các downstream service: consumer đã consume, đã xử lý, nhưng **không publish được**. Trong khi đó Kafka đã đi tiếp. Vậy là ta thấy input progress đã advance, nhưng downstream business state thì chưa được cập nhật. Lịch sử vẫn nằm đó, production vẫn đang tiếp tục, nhưng có một phần việc chưa hoàn thành mà ta vẫn phải quay lại sửa. Câu hỏi: sửa nó thế nào khi production vẫn đang xử lý bình thường như mong đợi?

## 4. Event không mất, production chỉ đã đi qua nó

Để minh họa trong một Kafka topic trông như thế nào, Ishan lấy một partition giả định với các offset đã được xử lý. Có hai offset mà ta không chắc business state đã được cập nhật hay chưa. Đồng thời, consumer group hiện tại đã ở offset 740. Nghĩa là production vẫn tiến lên với những gì tới tiếp theo, nhưng có vài event bị lệch và chưa được xử lý đến nơi.

![Slide 03 Progress: partition 12 offsets 731 tới 740, 732 và 736 missing output](https://homus.dev/photos/IMG_3920.JPG)

Slide "03 · Progress": "The event wasn't lost. Production had moved past it." Partition 12 với các offset minh họa 731 tới 740; offset 732 và 736 tô đỏ, ghi "missing output". Mũi tên xanh là production consumer group (Production CG) đã chạy tới 740. Mũi tên cam chỉ về phía đầu: "History still exists here." Dòng chính: "Retention gives you history. Consumer progress does not automatically revisit it." Retention của Kafka giữ lại lịch sử, nhưng việc consumer đã tiến lên không có nghĩa nó sẽ tự quay lại xem những chỗ hỏng.

Anh nhấn mạnh điều này trở nên cực kỳ quan trọng khi nói tới những ngành như **payments** hay **inventory**, vì nó dẫn thẳng tới trải nghiệm tệ cho khách hàng. Kafka lưu lại được offset qua [cơ chế retention và consumer group](https://kafka.apache.org/documentation/#intro_concepts_and_terms), nhưng nó không biết offset nào "đã xong về mặt business".

## 5. Ba thứ phải khớp nhau: retained, believed, audited

Ishan nêu ba khía cạnh ta luôn có khi nói về event-driven system, và anh nói rõ đây là bất kỳ event-driven system nào, không chỉ inventory:

- **Những gì được giữ lại (retained).** Theo ngôn ngữ Kafka: ta đang có những event nào. Theo một nghĩa nào đó đây là source of truth.

- **Những gì software tin (believed).** Ta làm rất nhiều xử lý và có một derived state dựa trên việc xử lý đó. Vậy software của ta đang tin điều gì?

- **Kiểm tra độc lập (audited).** Việc audit độc lập diễn ra thông qua physical inventory.

Ba nguồn sự thật Ishan nêu. Consumer có chạy nhanh tới đâu, lag bằng 0 đi nữa, nếu ba vòng này không trùng nhau thì hệ thống vẫn chưa đúng.

Nếu ba thứ này không khớp nhau, thì cho dù ta nói Kafka consumer đang xử lý record với tốc độ kỷ lục, không có chút lag nào, điều đó vẫn vô nghĩa. Vì business state của ta không khớp với những gì thực sự đang có trên sàn cửa hàng.

## 6. Hai cách sửa "dễ": replay tất cả, hoặc sửa tay

### Replay mọi thứ

Một ý nghĩ đến với Ishan khi nhóm đang xử lý incident: tại sao không đơn giản là quay lại replay tất cả? Đó có vẻ là cách dễ nhất. Kafka giữ lịch sử là có lý do, ta chỉ việc quay lại và replay. Nhưng một replay range, nếu làm không cẩn thận, có downstream effect riêng của nó.

Trong ví dụ, dựa vào range, ta có thể nói: tôi sẽ replay từ 731 tới 737, và việc đó sẽ đưa business state về đúng mong đợi. Nhưng không. Khi replay tất cả, ta bù được các effect bị thiếu, nhưng đồng thời cũng **"double dip" trên inventory** với những event đã thành công, những event đã đi xuống và cập nhật business rồi. Chúng bị áp dụng hai lần.

Replay cả khoảng 731 tới 737 để sửa hai offset hỏng (viền đỏ, 732 và 736) sẽ áp lại năm event đã thành công. Không có idempotency thì inventory bị cộng trừ hai lần.

### Sửa tay từng event

Cách thứ hai được bàn tới khi incident xảy ra: được thôi, tôi vào sửa tay event đó. 732 và 736 là hai event hỏng cần sửa. Nhưng hãy nghĩ xem incident thực chất là về cái gì. Incident thực chất là có vài khoảng trống dữ liệu nhỏ: hai event bị thiếu, không được phản ánh trong aggregated view mà business muốn nhìn thấy.

Khi làm bất cứ thứ gì bằng tay, và đây là việc nhóm anh đã làm một thời gian, luôn có rủi ro một **malformed event** bị publish vào topic. Nếu downstream consumer không đủ resilient, chúng sẽ gặp lỗi deserialization và không xử lý được gì nữa. Vậy là thay vì sửa một khoảng trống dữ liệu nhỏ, ta tạo ra một vấn đề lớn hơn: **toàn bộ production sập**.

Anh đưa một kịch bản: Nike tung ra một lô giày Jordan giới hạn. Đúng lúc đó production sập vì sự cố này. Website đang hiển thị còn 100 đôi, trong khi thực tế không còn đôi nào, và khách hàng không vui. Nên khi làm recovery hay retry, **blast radius** luôn là thứ phải kiểm soát.

## 7. Giống production nhưng không dùng production; Where, What, Whether

Vậy khi nghĩ tới việc xây một hệ thống retryable hay recovery, ta muốn nó **giống production**, nhưng **không muốn dùng chính production** cho việc đó. Có hai lý do. Giả sử vì một sự cố infrastructure nào đó, ta có một triệu event bị hỏng. Giờ ta có một hệ thống có thể replay một triệu event đó. Nhưng nếu replay một triệu event đó trên production, nó sẽ gây contention với chính production đang xử lý dữ liệu hiện tại.

Thứ ta cần là hai service độc lập dùng cùng một semantics. Tôi muốn dùng đúng cái artifact mà production đang dùng, nhưng muốn một pipeline cô lập riêng, một consumer group riêng có thể kiểm soát được. Tức là: khi xây recovery, ta muốn một hệ thống giống production, nhưng không đụng vào production, để production tiếp tục xử lý đúng việc nó đang làm.

Sau tất cả những điều trên, có ba thứ quan trọng nhất với nhóm anh:

![Slide 08 Identity: Where, What, Whether](https://homus.dev/photos/IMG_3922.JPG)

Slide "08 · Identity": "Where is it? What is it? Is it still valid?" Ba hộp: WHERE là Kafka coordinates (topic · partition · offset); WHAT là "same business operation" (event_id + business context*); WHETHER là "still valid in current state?" (ordering / current-state rules). Chú thích sao: business context là order · location · SKU · publication timestamp · XID where applicable. Dòng cuối: "Coordinates locate a record. They do not authorize its effect." Tọa độ Kafka giúp tìm ra record, nhưng không cho phép áp dụng effect của nó.

- **Where: lỗi nằm ở đâu.** Đó là Kafka coordinates: topic, partition, offset, hoặc timestamp mà lỗi xảy ra.

- **What: cái gì đang hỏng, và nó có được phản ánh vào business state không.** Khi nói về business state, **identity** trở nên rất quan trọng: đó là danh tính gắn với một event, để đảm bảo mọi downstream effect trùng lặp sẽ không bị áp dụng hai lần. Trong retail, hay nói chung trong event-driven system, ta gọi nó là idempotency ID, event ID. Nhưng khi xây service thật sự resilient, ta còn muốn thêm business context, mà trong retail được map xuống mức **item, location, SKU**. Như vậy ta có một key thống nhất để xây idempotency layer.

- **Whether: có nên replay hay không.** Việc này rất quan trọng vì ta không muốn event bị lệch thứ tự đi vào và phản ánh vào production một thứ đã xảy ra rồi. Quay lại ví dụ Jordan: có một event lúc 10:00 sáng bị lỗi, nói rằng còn 100 đôi sneaker. Nhưng ta đã xử lý một event lúc 10:05 nói rằng giờ chỉ còn 90 đôi. Vậy có nên quay lại replay event 10:00 để hiển thị 100 đôi không? Không. Timestamp cũng trở nên then chốt. Nên quyết định có replay event hay không cũng là một phần quan trọng.

Và vì nói tới identity, đó là chỗ ta cần một lớp có thể tin được, những lớp "edge" bảo vệ khỏi mọi bản trùng khi replay.

## 8. Idempotency hai lớp: Redis và DynamoDB, và recovery horizon

Quay lại recovery window: nếu ta xử lý lại 10 event, ta cần một **idempotency layer** bảo vệ để event không bị áp dụng hai lần. Vì một lần nữa, với inventory hay payment, ta không muốn một effect bị nhân đôi trên inventory state.

Cách nhóm anh xây là **idempotency hai lớp**, vì ta còn phải lo cả throughput và latency khi xây idempotency layer: từng event một đều phải đi qua bước kiểm tra này.

- **Lớp chính là Redis.** Nó cho biết event nào đang được xử lý ngay lúc này. Nếu vì lý do nào đó consumer consume một event hai lần, nó sẽ bị chặn ở tầng Redis, vốn chạy in-memory, scale được và nhanh hơn nhiều (kiểu khóa ngắn hạn như [SET với NX và thời hạn](https://redis.io/docs/latest/commands/set/)).

- **Lớp bền là DynamoDB.** Anh cũng cần một thứ để nếu ai đó hỏi "chuyện gì đã xảy ra một tháng trước", và nếu anh đang xử lý lại các event đó, thì có một cổng chặn đảm bảo chúng không bị trùng. Nhóm chọn DynamoDB làm durable state lâu dài cho các event đã xử lý (cách ghi có điều kiện: [DynamoDB condition expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html)). Họ **không lưu bản thân event**. Thứ duy nhất được lưu là idempotency key vừa nói ở trên. Nhờ đó, bất cứ lúc nào, kể cả hai tháng sau, nếu anh xử lý lại toàn bộ production, hệ thống chỉ xử lý lại đúng những event đã bị sót.

- Và dĩ nhiên họ dựa vào **Kafka**: dựa trên retention đặt cho topic, ta biết được những gì còn có thể replay.

![Slide 09 Recovery horizon: Redis 30 giây, DynamoDB durable, Kafka retained history](https://homus.dev/photos/IMG_3923.JPG)

Slide "09 · Recovery horizon": "Replay safety needs memory that outlives the retry window." Trục thời gian với ba thanh: Redis chỉ là "~30 sec processing guard" (chặn trùng trong khoảng 30 giây); DynamoDB là "durable processed-event evidence", kéo dài gần hết trục; Kafka là "retained history available for replay". Dòng cuối đổi câu hỏi: từ "Are we idempotent?" sang "For how long can we prove replay is safe?"

Một câu hỏi ta muốn trả lời khi nói về recovery window không chỉ là "vâng, mỗi service đều phải idempotent", mà là: **ta chứng minh được idempotency trong bao lâu?** Có chứng minh được lâu hơn sáu tiếng không, lâu hơn một tháng không? Vì với ví dụ physical inventory ở trên, PI diễn ra mỗi quý một lần, và nếu phát hiện ra một khoảng trống, ta cần có cách xử lý lại những record đã cũ ba tháng. Vậy nên cần cả idempotency hay edge layer tồn tại đủ lâu thì mới thành công được.

## 9. Tách execution, giữ nguyên semantics

Khi nói về việc tách recovery pipeline ra (anh tự sửa lời giữa chừng), đúng là ta muốn **cô lập execution**, nhưng ta cũng muốn **tái sử dụng semantics**. Ý anh là: ta không nên có một recovery pipeline dùng một artifact khác với artifact production đang dùng, vì cách business suy ra kết quả từ event sẽ thay đổi rất nhiều nếu làm vậy.

Isolate execution, reuse semantics: hai pipeline đọc cùng một topic nhưng dùng consumer group khác nhau, và cùng một artifact với cùng processing rules, validation, idempotency.

Thứ ta muốn là: **cùng processing rules, cùng validation, cùng idempotency**, chỉ là một pipeline riêng làm việc xử lý, để không làm nghẽn những gì production đang xử lý. Và dĩ nhiên là một **consumer group khác**, nếu không nó sẽ ảnh hưởng tới production pipeline.

## 10. Retry khác replay: retry topic và terminal DLQ

Ishan hỏi khán giả: có để ý là anh cố tình dùng từ "replay" và "recovery", sao không dùng "retry"? Vì chúng giải hai bài toán khác nhau.

**Retry** là cho transient failure, lỗi tạm thời. Ta muốn retry diễn ra tự động. Nhóm anh chia thành hai tầng. Nếu là lỗi tạm thời ngắn, chính app retry. Rồi có những tình huống như database sập một phút thì sao? Họ xây một pipeline mà event đi vào một staging topic, hay retry topic, nơi dùng exponential backoff và Spring retry để "đỗ" nó lại một lúc rồi xử lý lại (Spring Kafka có sẵn cơ chế này: [non-blocking retries với retry topic](https://docs.spring.io/spring-kafka/reference/retrytopic.html)). Cuối cùng, khi số lần thử đã chạm cấu hình tối đa và event được xem là non-retryable, họ gửi nó vào **terminal DLQ**.

Hai bài toán khác nhau: tầng retry tự động (app retry, rồi retry topic có backoff, cuối cùng là terminal DLQ), và recovery pipeline tách riêng dùng để replay có kiểm soát.

Họ gọi nó là **terminal** DLQ vì anh biết trong ngành người ta hay dùng DLQ để xử lý lại. Nhưng đúng nghĩa thì DLQ là dead letter queue: nó được thiết kế để là điểm cuối. Nó chỉ nên dùng cho audit, hoặc để hiểu vì sao lỗi xảy ra, chứ **không phải để replay**. Và đó là chỗ replay pipeline hay recovery pipeline bước vào.

## 11. Bốn trạng thái của recovery và recovery controller

Dùng chính topic đó, recovery đi qua bốn trạng thái:

- **Discover discrepancy:** phát hiện chênh lệch, tức là cố tìm ra timestamp, offset hay partition lúc lỗi xảy ra.

- **Scope:** khoanh vùng, tìm ra đúng cửa sổ, như trong ví dụ offset ở trên.

- **Verify safety:** kiểm tra xem có cần replay hay không. Các câu hỏi What, Whether và Where được trả lời ở bước này.

- **Recovery controller:** một pipeline riêng mà ta gọi theo nhu cầu (on demand), bằng cách đưa vào một khoảng offset và partition. Nó tự dựng Kafka consumer của riêng nó với một **consumer group ID mới**, xử lý lại các record đó, và idempotency sẽ gạt bỏ những gì không cần áp dụng lại.

Bốn trạng thái của một lần recovery theo Ishan: phát hiện chênh lệch, khoanh vùng, kiểm tra an toàn, rồi chạy recovery controller theo nhu cầu với một consumer group mới.

Anh đặt câu hỏi tiếp: khi recovery cho hơn một triệu event, việc rất phổ biến trong những ứng dụng scale lớn, thì ta cần cân nhắc những gì?

## 12. Một triệu event cần phục hồi cũng là production traffic

![Slide 12 Control: Scope, Rate, Stop, Watch](https://homus.dev/photos/IMG_3924.JPG)

Slide "12 · Control": "A recovery population of ~1 million events is production traffic." Bốn ô: SCOPE (partition · offset · time), RATE (protect downstream capacity), STOP (pause when invariants or capacity fail), WATCH (processed · skipped · failed · produced). Dòng cuối: "Old events use today's infrastructure." Event có cũ thì vẫn chạy trên hạ tầng của hôm nay.

- **Scope:** ta cần phạm vi, tức partition, offset, time.

- **Rate:** ta cần rate limiting. Ta vẫn muốn chắc rằng downstream xử lý kịp với tốc độ ta đẩy vào. Nếu anh replay một triệu event ngay bây giờ, và chúng vẫn đi vào đúng topic mà downstream consumer đang đọc, thì ta không muốn event lịch sử làm nghẽn event production hiện tại mà downstream đang consume. Việc này có thể giải bằng cách tự xây partitioning logic riêng, hoặc publish event vào một khoảng offset cụ thể trên outbound topic, để production thật tiếp tục từ chỗ nó đang ở, còn có một tập offset riêng chỉ gắn với lần recovery.

- **Stop:** khi thấy downstream consumer không scale kịp và consumer lag tăng cao, hoặc thấy lỗi bắt đầu xảy ra, đó là lúc cần pause hoặc dừng lại và quan sát.

- **Watch:** giống như production system mà ta vẫn theo dõi, **recovery system cũng là một production system**. Nên ta phải theo dõi những gì đã được processed, skipped, failed và produced. "Skipped" là những event ta biết không còn cần xử lý xuống durable state nữa, nhưng ta vẫn ghi nhận chúng để có một lịch sử giao dịch rõ ràng của các record về phía mình.

Như slide nói: event thì cũ, nhưng chúng vẫn dựa vào infrastructure production của hôm nay.

## 13. Structural check và runbook: Scope, Isolate, Execute, Prove

Điều cuối cùng, sau khi đã làm hết những việc trên, là một **structural check**. Vì sao nó quan trọng: PI nói rằng aggregated inventory phải là 75. Sau recovery, hệ thống nói cũng là 75. Nhưng ta cần chắc rằng mình không nhân đôi một record cụ thể nào đó đồng thời bỏ sót một record khác, hai lỗi bù trừ nhau để tổng vẫn đúng. Hệ thống vừa mô tả xác nhận điều đó không xảy ra, nhưng khi đang đi qua giai đoạn recovery, có sẵn bước kiểm tra này vẫn tốt hơn.

Dựa trên toàn bộ talk, nếu phải để lại một runbook, Ishan chọn bốn việc quan trọng nhất khi nói về replay:

Runbook bốn bước Ishan để lại cho khán giả khi phải replay một event-driven system.

- **Scope:** tìm discrepancy, giới hạn phần lịch sử cần điều tra bằng offset, partition, timestamp.

- **Isolate:** giữ production chạy nguyên như cũ, và cô lập recovery thành một pipeline độc lập, tách khỏi production.

- **Execute:** dùng các production safety check, dùng cùng artifact với production, và thực thi toàn bộ replay window bằng một controller chạy tay hoặc on demand.

- **Prove:** khi retry xong, reconcile **effect** chứ không chỉ reconcile **tổng số**, và chỉ đóng incident khi không còn điều gì bất định.

## 14. Lời kết: Kafka giữ lịch sử, nhưng không nói cần phục hồi cửa sổ nào

Ishan quay về điểm xuất phát. Kafka có lịch sử, và đó là phần quan trọng nhất với nhóm anh: Kafka giữ lại lịch sử, nhưng **nó không nói cho ta biết cửa sổ nào cần dùng để recovery**. Nên dùng lịch sử đó trong Kafka, rồi áp các yếu tố khác như idempotency, rate limiting, production safeguard để thực thi sao cho không bị trùng lặp. Và cuối cùng ta reconcile để khép vòng lặp, và bằng chứng độc lập (independent evidence) xác nhận việc sửa chữa.

Vòng khép lại của talk: lịch sử trong Kafka, các lớp bảo vệ, thực thi, rồi reconcile với bằng chứng độc lập để chứng minh việc sửa.

Anh để lại cho khán giả một điều để nghĩ: lần tới khi review bất kỳ event-driven microservice nào, đừng chỉ nghĩ về latency, về consumer lag, hay "nếu infrastructure sập thì có xử lý lại được không". Hãy nghĩ thêm: nếu sáu tháng nữa, hay hai tiếng nữa, có người đặt câu hỏi về state của hệ thống, **ta có cách nào chứng minh nó vẫn đúng không?** Anh cảm ơn khán giả.

Người dẫn chương trình cảm ơn Ishan, báo rằng sẽ có một quãng nghỉ và Stage 3 quay lại lúc 1:30 với session tiếp theo. Ai có câu hỏi cho Ishan thì cứ ra ngoài trò chuyện trực tiếp với anh.

## Nguồn và link

- [Trang session chính thức WeAreDevelopers](https://www.wearedevelopers.com/world-congress-north-america/agenda/sessions/replay-safe-architecture-building-event-driven-systems-that-can-recover-with-con-1280580) (abstract nhắc thêm outbox pattern, replay-safe consumer, audit trail, reconciliation workflow)

- HackerNoon, [Ishan Shah on Recoverable Systems, AI Guardrails, and the Internet's Useful Weirdness](https://hackernoon.com/ishan-shah-on-recoverable-systems-ai-guardrails-and-the-internets-useful-weirdness); LinkedIn: linkedin.com/in/ishandshah

- Kafka: [Main concepts and terminology (topic, partition, offset, retention, consumer group)](https://kafka.apache.org/documentation/#intro_concepts_and_terms)

- CDC: [Oracle GoldenGate](https://www.oracle.com/integration/goldengate/) · [Debezium](https://debezium.io/)

- Idempotency: [Redis SET (NX, EX)](https://redis.io/docs/latest/commands/set/) · [DynamoDB condition expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html)

- Retry: [Spring for Apache Kafka, non-blocking retries (retry topic, DLT)](https://docs.spring.io/spring-kafka/reference/retrytopic.html)
