---
title: "Thiết kế hệ thống event-driven - message queue, pub/sub, saga, CQRS và event sourcing"
description: "Khoá học về thiết kế hệ thống event-driven, xoay quanh cách consumer đọc event: chia nhau message, nhận bản sao riêng hay đọc lại từ log. Bạn sẽ biết chọn partition key, xử lý lag và poison message, thiết kế saga, replay event cũ đúng schema, đếm đúng khi event đến muộn và biết khi nào cần CQRS hay event sourcing."
canonical: "https://200lab.io/courses/thiet-ke-he-thong-event-driven-message-queue-pub-sub-saga-cqrs-va-event-sourcing"
type: "course"
course_type: "course"
learning_type: "free_form"
published: "2026-10-06T15:07:50Z"
students: 1
---

## Khoá học này mang lại gì cho bạn

Lúc hệ thống còn nhỏ, service đơn hàng gọi thẳng vài service khác hoặc đẩy job vào queue cho worker. Rồi thêm team thứ ba, team thứ năm cũng cần biết mỗi khi có đơn mới, giá đổi hay một shop bị khoá. Từ lúc đó, event đi qua broker tới những consumer mà producer không biết hết.

Bảy kỹ năng dưới đây giúp bạn thiết kế những event đó, để khi thêm consumer, bạn không phải sửa producer, còn consumer vẫn xử lý đúng khi event đến muộn, bị gửi trùng, đến sai thứ tự hay được đọc lại sau nhiều tháng.

- **Chọn queue, pub/sub hay event log** — Bạn phân biệt event với command, rồi chọn mô hình theo cách consumer đọc: message bị xóa khi một consumer xử lý xong, mỗi subscription nhận một bản sao, hay message nằm lại trong log để mỗi consumer group đọc theo vị trí riêng. Bạn cũng biết khi nào một bảng Postgres mà consumer đọc theo cursor là đủ, và khi cần broker thì chọn broker nào hợp với constraint vận hành của team.
- **Thiết kế nội dung của event** — Bạn đưa vào payload những field mà consumer cần, kèm `version` và thời điểm xảy ra, để consumer cập nhật bản sao của mình từ chính event thay vì gọi ngược về producer, và khi đọc lại event cũ vẫn thấy đúng dữ liệu lúc event xảy ra.
- **Giữ đúng thứ tự và theo kịp tải** — Bạn chọn partition key theo phạm vi cần giữ thứ tự, dùng version để chặn event đến muộn ghi đè trạng thái mới hơn, và tính số partition cho giờ cao điểm. Bạn cũng đưa message lỗi vào DLQ để nó không làm kẹt cả partition, và đặt retention đủ dài cho lần consumer dừng lâu nhất.
- **Điều phối luồng nhiều bước** — Bạn chọn choreography hay orchestration cho một luồng đi qua nhiều service, chạy compensation khi một bước lỗi, và gắn correlation ID, causation ID để lần ra event đã mở đầu một chuỗi event.
- **Replay và đổi schema của event** — Bạn replay event cũ vào bảng mới bằng một consumer group mới mà không gửi lại email hay notification cũ, giữ contract của event trong AsyncAPI và schema registry, và dùng upcaster để event cũ vẫn được đọc theo nghĩa lúc nó được ghi.
- **Đếm event cho đúng** — Bạn đếm theo thời điểm event xảy ra thay vì lúc hệ thống nhận được, xử lý event đến muộn và event bị gửi trùng, và quyết định số nào được phép là ước lượng bằng HyperLogLog, số nào phải đếm chính xác vì dùng để tính tiền.
- **Chọn CRUD, CQRS hay event sourcing** — Với từng service, bạn so sánh ba cách lưu dữ liệu này theo câu hỏi mà dữ liệu của service đó phải trả lời, và nhận ra khi nào CRUD kèm audit log là đủ. Với service thật sự cần event sourcing, bạn ghi event có kiểm tra expected version, dùng snapshot cho stream dài và đổi cách đọc event đã lưu bằng upcaster thay vì sửa lịch sử.

Code minh họa viết bằng TypeScript, dựa trên một interface `consume(topic, group, handler)` do khoá học tự định nghĩa, nên không gắn với một broker hay client library nào; tên công cụ chỉ dùng để gọi tên khái niệm, trừ một bài so sánh các broker với nhau.

## Khoá học cung cấp những nền tảng nào

Cả khoá học dựa trên một khái niệm: **event**, tức một việc đã xảy ra, được producer ghi lại một lần rồi giao cho mọi consumer cần biết. Khác với một lời gọi API, producer không biết consumer nào đang đọc event, còn consumer không hỏi lại producer được. Event còn được giữ trong log rất lâu, thường lâu hơn cả phiên bản code đã ghi ra nó.

Mỗi bài bắt đầu từ một triệu chứng ở consumer hoặc broker, rồi chỉ ra event và topology đúng có dạng ra sao, consumer thấy gì thay đổi, tradeoff là gì, và ở nấc nào của bậc thang scale thì chưa cần cơ chế đó.

Bài [What do you mean by “Event-Driven”?](https://martinfowler.com/articles/201701-event-driven.html) của Martin Fowler tách bốn kiểu hệ thống hay bị gọi chung là event-driven. Bài [The Log: What every software engineer should know about real-time data’s unifying abstraction](https://web.archive.org/web/20240105095933/https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying) của LinkedIn giải thích vì sao một log mà mỗi hệ thống đọc theo vị trí riêng trở thành nền của việc chuyển dữ liệu giữa nhiều hệ thống. Ghi chú [CQRS](https://martinfowler.com/bliki/CQRS.html) nhắc rằng với phần lớn hệ thống, CQRS chỉ thêm độ phức tạp.

Sáu nhóm bài dưới đây đi theo thứ tự hệ thống lớn dần, từ consumer đầu tiên tới event store. Những bài nền cần thiết từ các khoá System Design khác được đặt ngay trước bài đầu tiên dựa vào chúng. Bảy bài tình huống ghép nhiều cơ chế vào một hệ thống cụ thể, có số đo trước và sau khi sửa.

### 1. Chọn queue, pub/sub hay event log theo cách đọc của consumer

Nhóm này mở đầu bằng năm bài nền, rồi tới bốn quyết định cần có trước khi consumer đầu tiên chạy. Năm bài nền gồm:

- queue và worker;
- source of truth và dữ liệu dẫn xuất;
- outbox;
- chống trùng khi broker giao lại message;
- cách đọc timeline của hai request chạy song song.

Git log của service đơn hàng có **9** lần deploy trong một quý, lần nào cũng vì việc của team khác. Có lần team SMS đổi tên queue, và **26.000** command gửi SMS đổ vào queue cũ suốt **3 giờ** mà không consumer nào đọc. Quyết định đầu tiên là phân biệt event với command, rồi chỉ ra service nào sở hữu event, để mỗi lần có thêm consumer, service đơn hàng không phải deploy lại.

Một sáng có **14.200** đơn được tạo mà service hóa đơn chỉ xử lý **7.080** message, log vẫn ghi `errors=0`. Service phân tích vừa bắt đầu đọc chung queue đó, và queue chia message giữa các consumer cùng đọc nó. Bài tiếp theo so sánh queue, pub/sub và event log theo hai câu hỏi: message được giao cho consumer nào, và được giữ tới bao giờ.

Khi chưa cần broker, một bảng Postgres có thể làm event log. Một consumer đọc bảng đó theo `id` đã bỏ sót **330** trong **820.000** đơn, nên các đơn này không được tính phí hoa hồng. Nguyên nhân là một transaction commit muộn **1.780 ms** đã ghi ra dòng có id nhỏ hơn vị trí cursor đã đọc tới. Bài trình bày cách chỉ đọc những dòng do transaction đã kết thúc ghi ra, và tradeoff khi có transaction mở lâu.

Quyết định cuối của nhóm là event mang những gì. Event `product.updated` chỉ mang `productId`, nên khi một người bán sửa giá **240.000 SKU**, sáu consumer gọi thêm **1,44 triệu** lần `GET /products/{id}` trong **10 phút**, và p99 của catalog API tăng từ **120 ms** lên **4,2 giây**. Bài so sánh event chỉ mang id với event mang trạng thái lúc xảy ra.

### 2. Partition key, consumer group, lag và poison message

Khi nhiều consumer xử lý song song, event bắt đầu đến sai thứ tự. Mỗi ngày có **0,7%** trong **200.000 đơn** bị lùi trạng thái trên app: đơn vừa “đã bàn giao vận chuyển” lại về “đang đóng gói”. Bài đầu tiên chọn partition key theo phạm vi cần giữ thứ tự, và giải thích vì sao những event cần đúng thứ tự với nhau phải nằm cùng topic và cùng key.

Partition key chưa đủ khi event đi qua retry topic. Một shop bị khoá lúc **14:00:02**, nhưng năm phút sau, bản sao trong service checkout lại ghi shop đó đang mở. Khách vẫn đặt thêm **37 đơn** vào shop này. Bài thứ hai thêm một điều kiện version vào câu `UPDATE`, để event cũ đến muộn không ghi đè trạng thái mới hơn.

Bài tình huống đầu tiên gộp profile ẩn danh vào profile của tài khoản khi khách đăng nhập. **18%** khách đã đăng nhập có hai profile, và mỗi ngày **2.300 khách** nhận email nhắc mua lại món hàng vừa thanh toán. Cách sửa dùng partition key, version và chống trùng cùng một bảng alias, để event gửi muộn mang id ẩn danh cũ vẫn vào đúng profile đã gộp.

Ba bài tiếp theo xử lý tải và sự cố. Một consumer group tăng từ **6** lên **12 instance** mà throughput vẫn **90 event/giây** và lag lên **504.000 event**, vì mỗi partition chỉ được giao cho một consumer trong group. Một event có field `weight` sai kiểu làm partition 5 đứng yên **2 giờ**, trong khi cảnh báo lag trung bình vẫn báo bình thường.

Một loader dừng **75 giờ** để bảo trì data warehouse, trong khi topic chỉ giữ event của **72 giờ**, và **500.000** biến động tồn kho biến mất không kèm dòng lỗi nào. Ba bài này trình bày cách tính số partition, chỉ retry lỗi tạm thời rồi chuyển phần còn lại sang DLQ, và đặt retention, disk, cảnh báo theo lần dừng lâu nhất của consumer.

Hai bài cuối nhóm là bài nền về rate limiting và bài tình huống gửi notification cho hàng triệu người nhận. Một shop có **1,2 triệu người theo dõi** bắt đầu livestream, SMS mã OTP đăng nhập đến trễ hơn **14 phút**, provider SMS trả 429 cho **31%** lời gọi, còn **6%** người theo dõi nhận hai push giống nhau. Bài tách notification giao dịch khỏi đợt gửi lớn, chia đợt gửi thành lô, và giữ tốc độ gửi dưới hạn mức của từng provider.

### 3. Luồng nghiệp vụ nhiều bước chạy qua nhiều service bằng event

Một sản phẩm mới đi qua năm service trước khi lên sàn, mỗi service chờ event của bước ngay trước. Mỗi ngày khoảng **276 sản phẩm** kẹt quá **24 giờ**, và không truy vấn nào cho biết chúng đang ở bước nào. Nhóm bắt đầu bằng việc so sánh choreography với orchestration, và chỉ ra luồng nào hợp với cách điều phối nào.

Throughput của hai topic tăng từ **300** lên **45.000 event/giây** trong **6 phút**, disk của broker đã ở mức **80%**. Các service đang phản ứng với event của nhau thành một vòng lặp, và log có đủ **2,1 triệu event** nhưng kỹ sư trực không nối được event nào với event đã gây ra nó. Bài thứ hai gắn correlation ID, causation ID và độ sâu của chuỗi vào mỗi event, để tìm ra event đã mở đầu vòng lặp và chặn vòng lặp trước khi disk đầy.

Sau bài nền về compensation là bài tình huống về một luồng đặt hàng chạy bằng event. Trong **27.500 đơn** mỗi ngày, **55 đơn** đã hủy vì thanh toán lỗi mà vẫn được giao, còn suất hàng của các đơn thanh toán lỗi khác bị giữ tới nửa đêm. Bài kết hợp orchestration, outbox, chống trùng và compensation, để chỉ bắt đầu giao hàng sau khi đơn đã thanh toán.

### 4. Replay event cũ theo đúng schema lúc event được ghi

Code đổi qua nhiều phiên bản trong khi event cũ vẫn nằm trong log, nên đọc lại event cũ là việc thường gặp. Một team reset offset về **4 ngày** trước để tính lại một bảng số liệu sai, và **3.400** shop nhận lại email cảnh báo về chuyện đã qua. Bài đầu tiên của nhóm replay vào bảng mới bằng một consumer group mới, ước lượng thời gian đọc lại **10 triệu** event, và đối chiếu bảng mới trước khi chuyển truy vấn sang.

Bài tình huống tiếp theo bắt đầu từ một mẫu **10.000 SKU**, trong đó có **180 SKU** mà giá hoặc số tồn kho trên kết quả search khác với trang sản phẩm. Bài dùng `version` để xếp từng SKU lệch vào đúng một trong ba nguyên nhân chồng lên nhau:

- event mới chưa được indexer đọc tới;
- event cũ đi qua retry topic rồi ghi đè event mới;
- event nằm trong DLQ mà không ai redrive.

Hai bài kế đó nói về schema của event. Một diff hai dòng đổi `address` từ chuỗi sang object, và consumer của một team mà không ai biết tới đã dừng **6 giờ**. Bài trình bày cách ghi contract của event trong file AsyncAPI, rồi dùng schema registry với compatibility mode để CI chặn thay đổi đó trước khi merge.

Schema registry chặn được việc đổi kiểu dữ liệu, nhưng không nhận ra khi nghĩa của một field thay đổi. Replay event của **90 ngày** cho ra GMV tháng 8 là **40,66 tỷ đồng**, trong khi số đã chốt là **38,0 tỷ đồng**, vì event version 1 có `total` gồm cả phí giao hàng. Bài đặt một upcaster ngay sau bước parse, để code tính GMV chỉ biết một nghĩa của `total`.

Hai bài tình huống tiếp theo dùng replay và version của event trên những hệ thống cụ thể. Sau một lần replay, profile của **212 người** đã yêu cầu xóa dữ liệu xuất hiện trở lại, và **140 người** trong số đó vẫn nhận email marketing. Bài lần theo dữ liệu cá nhân trong log, dữ liệu dẫn xuất và các công cụ bên ngoài, rồi ghi trạng thái xóa riêng cho từng bản sao.

Bài tình huống còn lại nạp lại tài liệu cho trợ lý RAG khi đổi embedding model. Suốt **27 giờ** embed lại, tỉ lệ câu hỏi mẫu mà trợ lý không tìm ra tài liệu đúng tăng từ **8%** lên **27%**, và provider tính tiền **2.400.000** lời gọi cho **1.560.000** chunk khác nhau. Bài dựng bảng mới bằng một consumer group riêng và chỉ chuyển truy vấn khi kết quả của bảng mới trên bộ câu hỏi mẫu không kém bảng cũ.

Chín consumer cùng poll bảng `events`, các truy vấn poll chiếm **18%** CPU của Postgres primary, và trong một tuần team nhận ba đề xuất khác nhau. Bài chọn broker so sánh RabbitMQ, Kafka, NATS JetStream và Redis Streams, lấy bảng Postgres làm mốc, theo cách consumer đọc event và việc vận hành mà team gánh được.

### 5. Đếm event cho đúng khi event đến muộn hoặc bị gửi trùng

Lượt xem khung **20:00-21:00** của một shop được chốt ở **1.408**, còn replay lại ngày đó cho ra **1.483**. App giữ event lại khi điện thoại mất mạng rồi gửi sau, và SDK gửi trùng khi retry. Cách đếm đúng gán mỗi lượt xem vào giờ người dùng xem. Với khung **20:00-21:00**, consumer chỉ chốt số khi đã đọc tới các event mà hệ thống nhận sau **21:30**, tức **30 phút** sau khi giờ đó kết thúc. Lượt xem nào đến sau lúc chốt được ghi thành dòng điều chỉnh.

Cộng số người xem khác nhau của bảy ngày ra **410.000**, trong khi cả tuần chỉ có **260.000** người. Truy vấn đếm chính xác thì mất **40 giây** trên một bảng **1,8 tỷ** dòng. Bài thay bảng đó bằng sketch HyperLogLog khoảng **12 KB** mỗi shop mỗi ngày, gộp được theo tuần, và chỉ ra số nào được phép là ước lượng.

Bài tình huống của nhóm này liên quan tới tiền. Tháng 9, chi phí quảng cáo trên dashboard của người bán cao hơn hóa đơn **1,7%**, khoảng **936 triệu đồng**, và **1.240** trong **5.200** shop thấy số chênh lệch hơn **1%**. Bài tách khoản chênh thành ba nguyên nhân, rồi dùng một định nghĩa click hợp lệ chung cho dashboard và hóa đơn, đếm chính xác theo event time và đối chiếu bằng replay.

### 6. CQRS, event sourcing và khi nào CRUD là đủ

Nhóm cuối bắt đầu từ một service mà team đã xây bằng event sourcing. Service hồ sơ người bán có **14** truy vấn, trong đó **11** truy vấn chỉ cần dữ liệu hiện tại, vậy mà mỗi field mới tốn khoảng **3 ngày công** vì phải sửa từ event tới projection. Bài so sánh CRUD kèm audit log, CQRS và event sourcing theo câu hỏi mà dữ liệu của từng service phải trả lời.

Ba bài sau dành cho service thật sự cần event sourcing. Một khung giờ nhận hàng chỉ có **8 slot** mà có **9 xe** tới kho, vì hai command cùng thấy còn một chỗ trống; bài giải thích aggregate là gì và vì sao mỗi lần ghi phải kèm expected version. SKU bán chạy nhất có **40.000 event**, nên mỗi lần quét hàng mất khoảng **2,8 giây** chỉ để nạp stream; bài dùng snapshot cho stream dài.

Bài cuối khoá bắt đầu từ một câu `UPDATE` điền lý do cho **1,2 triệu** event đã lưu, sau đó job đối chiếu hằng tuần báo **312 SKU** có hai số tồn kho khác nhau. Bài đổi cách đọc event cũ bằng upcaster khi nạp stream, và so sánh với việc chép event sang stream mới, để event store vẫn là dữ liệu gốc không bị sửa.

## Vì sao 200Lab tạo ra khoá học này

Queue và worker là cách quen thuộc để đưa một việc xử lý lâu ra khỏi request. Nhưng khi nhiều team cùng cần phản ứng với một việc vừa xảy ra, queue không còn đủ. Mỗi team mới lại bắt bạn sửa và deploy lại service của mình, hoặc consumer của team đó lặng lẽ lấy mất một nửa số message của consumer cũ.

Hệ thống event-driven thường hỏng mà không báo lỗi. Consumer không ném exception, log ghi `errors=0`, cảnh báo lag trung bình vẫn xanh, còn đơn thiếu hóa đơn, trạng thái đơn lùi lại hay event bị xóa trước khi được đọc thì chỉ lộ ra ở báo cáo cuối tháng. Lúc đó bạn là người ngồi đối chiếu từng đơn, giải thích với người bán vì sao dashboard và hóa đơn lệch nhau, và dựng lại phần dữ liệu đã mất.

Khoá học giúp bạn chặn những lỗi đó từ lúc thiết kế, thay vì phát hiện chúng qua báo cáo cuối tháng. Mỗi sự cố trong khoá được truy ngược về một quyết định cụ thể, như chọn partition key, kiểm tra version khi ghi hay đặt retention cho topic. Đi kèm mỗi quyết định là những câu hỏi bạn phải trả lời bằng số liệu của chính hệ thống mình. Phạm vi nào cần giữ thứ tự, retention dài bao nhiêu, số nào được phép ước lượng? Và ở quy mô hiện tại, cơ chế nào bạn chưa cần dùng tới?

Học xong khoá học, mỗi khi thêm consumer hay đổi một event, bạn tự trả lời được những consumer nào đang đọc event này, event của cùng một thực thể có cần đúng thứ tự không, và consumer có phải đọc lại event cũ không. Bạn cũng có cơ sở để nói một bảng Postgres là đủ, hay CRUD kèm audit log là đủ, thay vì dựng broker hay event sourcing chỉ vì nghe nói hệ thống lớn đều dùng.

## Những vấn đề thường gặp khi thiết kế hệ thống event-driven

Khi một consumer cho ra số sai, việc đầu tiên nhiều team làm là tìm lỗi trong code xử lý, từ hàm parse tới câu SQL. Với hệ thống event-driven, nguyên nhân thường nằm ở cách broker giao message, ở thứ tự event đến consumer, hoặc ở event cũ được đọc lại theo nghĩa mới.

- Service phân tích bắt đầu đọc queue `order-placed`, và **7.120** trong **14.200** đơn không có hóa đơn điện tử dù log ghi `errors=0` — queue giao mỗi message cho một consumer, nên thêm service thứ hai vào cùng queue là chia việc, không phải thêm một service nhận đủ message.
- Consumer đọc bảng event theo `id` không báo lỗi nào, nhưng **330** trong **820.000** đơn không có phí hoa hồng — transaction commit muộn ghi ra dòng có id nhỏ hơn vị trí cursor đã đọc tới, nên lượt đọc sau không bao giờ thấy dòng đó.
- Consumer group tăng từ **6** lên **12 instance** mà throughput vẫn **90 event/giây** — trong một consumer group, mỗi partition chỉ được giao cho một consumer, nên với topic có **6** partition, **6 instance** thêm vào không nhận được partition nào.
- Topic về shop đã chia partition theo `shop_id`, vậy mà năm phút sau khi shop bị khoá lúc **14:00:02**, bản sao trong service checkout lại ghi shop đó đang mở — event đi qua retry topic không còn theo thứ tự của partition, và consumer ghi mà không so sánh version.
- Cảnh báo lag trung bình không bật lên trong khi partition 5 đứng yên **2 giờ**, còn **31.000 event** chưa được consumer đọc tới — một event có field sai kiểu bị retry mãi, chặn mọi event phía sau nó trong partition, và lag trung bình của **12** partition che mất partition bị kẹt.
- Loader chạy lại sau **75 giờ** bảo trì, không có dòng lỗi nào, nhưng **500.000** biến động tồn kho không vào data warehouse — topic chỉ giữ event của **72 giờ**, nên event của ba giờ đầu đã bị xóa trước khi loader đọc tới.
- Một team reset offset để tính lại số liệu của **4 ngày**, và **3.400** shop nhận lại email cảnh báo về chuyện đã qua — consumer dựng bảng cũng là consumer gửi email, nên replay chạy lại cả việc gửi email.
- Replay event của **90 ngày** làm GMV tháng 8 cao hơn số đã chốt khoảng **7%** — event version 1 có `total` gồm cả phí giao hàng, và code viết cho version 2 đọc nó theo nghĩa mới.

Những sự cố trên đều có bài riêng trong khoá. Ở đó, bạn xem log, payload và số liệu của chính sự cố, tìm ra cơ chế gây lỗi, rồi chọn cách sửa kèm tradeoff.

## Bạn sẽ học theo cách nào

- **Bắt đầu từ tín hiệu kỹ thuật** — Bài học mở bằng thứ bạn thấy được khi vận hành, như biểu đồ lag theo partition hay vài dòng log của consumer, kèm câu hỏi chuyện gì đang xảy ra.
- **Payload JSON, sơ đồ và ảnh minh họa** — Event được viết thành payload JSON đầy đủ, consumer được viết bằng đoạn TypeScript ngắn, còn sơ đồ gọn và ảnh minh họa cho thấy event đi từ producer qua broker tới từng consumer group theo thời gian.
- **Chốt quyết định bằng số liệu** — Partition key, số partition, retention, compatibility mode hay cách lưu dữ liệu đều được chọn từ số liệu của ví dụ, kèm tradeoff của từng lựa chọn.
- **Quiz yêu cầu bạn dự đoán** — Nhiều bài cơ chế có quiz ngắn, như đoán event nào được ghi sau cùng khi hai instance xử lý cùng một đơn, hay số đếm của một giờ đổi thế nào sau khi replay.
- **Demo kit và event lab chạy bằng Docker** — Bảy bài tình huống có bảy demo kit, mỗi kit là một file Markdown cho coding agent của bạn dựng hệ thống ở trạng thái chưa sửa bằng ngôn ngữ bạn chọn, cho triệu chứng hiện ra, rồi đo lại sau khi bạn tự sửa theo bài. Các bài cơ chế dùng chung một event lab với **13** kịch bản, mỗi kịch bản tái hiện cơ chế của một bài; kit và lab không chứa lời giải và không bắt buộc.

## Khoá học dành cho ai

1. **Developer backend đã dùng queue** — Bạn đã viết service lưu dữ liệu vào Postgres và đẩy việc ra queue cho worker, nay nhiều service của nhiều team phải phản ứng với cùng một việc như đơn mới, giá đổi hay shop bị khoá, và bạn cần để team khác tự nhận event mà không phải sửa service của mình.
2. **Người vận hành broker** — Bạn gặp lag tăng vào giờ cao điểm, event của cùng một thực thể đến sai thứ tự hay một message lỗi làm kẹt consumer, và cần dựa vào triệu chứng trên dashboard để quyết định số partition, cách dùng DLQ và retention dài bao lâu.
3. **Người làm saga hoặc dashboard** — Bạn phụ trách một luồng như đặt hàng đi qua nhiều service, hay một dashboard đếm lượt xem, click từ event, và cần số liệu khớp với source of truth.
4. **Người cân nhắc event sourcing** — Team bạn có đề xuất dùng CQRS hay event sourcing, và bạn cần tiêu chí để quyết định cho từng service, kể cả khi câu trả lời là CRUD kèm audit log.

## Những điều cần lưu ý

- **Kiến thức nền cần có** — Bạn chỉ cần từng viết service nhận request và ghi vào Postgres trong transaction, đọc được TypeScript, SQL và log; không cần biết trước Kafka, RabbitMQ hay NATS. Những bài nền về queue, outbox, chống trùng, rate limiting và compensation đã nằm sẵn trong khoá, ngay trước bài cần đến chúng.
- **Nên học thêm các khoá System Design liên quan** — Những bài nền trên lấy từ các khoá “Thiết kế hệ thống — ước lượng tải, load balancer, cache, queue, replication và partitioning”, “Xử lý timeout, retry và crash — idempotency, outbox, lease và reconciliation” và “Thiết kế hệ thống chịu tải cao — rate limiting, load shedding và circuit breaker”. Học trọn ba khoá đó cùng khoá “Thiết kế API giữa các service — error model, versioning và contract test”, bạn nắm kiến thức đầy đủ hơn về retry và idempotency key, cách lưu kết quả từng bước và đối chiếu, load shedding và contract giữa các service, những phần mà ở đây chỉ được nhắc qua.
- **Không đi vào vận hành cluster hay cấu hình công cụ** — Stream processing chuyên sâu, vận hành cluster broker, replication, clock và cấu hình chi tiết của từng broker không có trong khoá học; đổi lại, bạn hiểu cơ chế đủ rõ để biết mỗi công cụ đang quyết định thay bạn những gì.
- **Số liệu trong ví dụ do bài tự đặt** — Số đơn, số event mỗi giây hay tỉ lệ lỗi được chọn để các phép tính khớp nhau, không phải số đo từ production của một công ty nào; khi áp dụng, bạn đo lag, throughput và độ trễ của event trên hệ thống của mình rồi đưa vào cùng cách tính.

## Bắt đầu từ đâu

Khoá học mở đầu bằng request tạo đơn có p99 đã tăng từ **280 ms** lên **2,1 giây**, và trace của một request quanh mức p99 cho thấy sau bước lưu đơn là năm lời gọi nối tiếp tới service của năm team khác. Bài đầu tiên trả lời vì sao mỗi lời gọi thêm vào làm request vừa chậm hơn vừa dễ lỗi hơn, event-driven đổi điều gì, và hệ thống của bạn đang ở nấc nào của bậc thang scale. Câu trả lời đó cho biết bạn cần queue, pub/sub hay event log, hay vẫn chưa cần gì cả.

## Chương trình

- Khi nào hệ thống cần chuyển sang event-driven (miễn phí)

### Chọn queue, pub/sub hay event log theo cách đọc của consumer

- Dùng queue để API không phải chờ những việc xử lý lâu · quiz
- Source of truth: dữ liệu nào là gốc, dữ liệu nào dựng lại được
- Ghi database và gọi service bên ngoài không nằm chung một transaction · quiz · kit cho agent
- Đã dùng queue mà mỗi lần thêm consumer vẫn phải sửa producer (miễn phí)
- Chặn consumer xử lý một message hai lần khi queue giao lại · quiz
- Hai service đọc chung một queue thì mỗi service chỉ nhận một phần message (miễn phí) · quiz · kit cho agent
- Đọc timeline của hai request chạy song song để tìm race condition (miễn phí)
- Đọc bảng event theo id khiến consumer bỏ sót event commit muộn · quiz · kit cho agent
- Chọn dữ liệu đưa vào event để consumer không phải gọi ngược về producer

### Partition key, consumer group, lag và poison message

- Chọn partition key để event của cùng một đơn được xử lý đúng thứ tự · quiz · kit cho agent
- Dùng version để chặn event đến muộn ghi đè trạng thái mới hơn · quiz · kit cho agent
- Gộp profile ẩn danh vào profile của tài khoản khi khách đăng nhập · kit cho agent
- Chọn số partition để consumer group theo kịp tải giờ cao điểm · quiz · kit cho agent
- Xử lý poison message để một event lỗi không làm kẹt cả partition · quiz · kit cho agent
- Event chưa đọc bị xóa khi consumer dừng lâu hơn retention · quiz · kit cho agent
- Dùng rate limiting để một client không làm chậm mọi người dùng khác · quiz
- OTP đến trễ trong đợt gửi notification cho hàng triệu người · kit cho agent

### Luồng nghiệp vụ nhiều bước chạy qua nhiều service bằng event

- Chọn choreography hay orchestration cho luồng nhiều bước chạy bằng event
- Lần theo chuỗi event để tìm event đã mở đầu một vòng lặp · quiz · kit cho agent
- Hoàn tác các bước đã xong khi bước sau thất bại · quiz
- Luồng đặt hàng chạy bằng event giao luôn cả đơn đã hủy vì thanh toán lỗi · kit cho agent

### Replay event cũ theo đúng schema lúc event được ghi

- Tính lại dữ liệu bị sai nhiều ngày bằng cách replay event · quiz · kit cho agent
- Giá và tồn kho trong kết quả search khác với trang sản phẩm · kit cho agent
- Đổi field trong event mà không làm hỏng consumer của team khác · quiz
- Event cũ bị đọc theo nghĩa mới khi replay · quiz · kit cho agent
- Xóa dữ liệu của một khách khỏi event log và mọi bản sao · kit cho agent
- Nạp lại tài liệu cho RAG khi đổi embedding model · kit cho agent
- Chọn broker theo cách đọc của consumer và constraint vận hành

### Đếm event cho đúng khi event đến muộn hoặc bị gửi trùng

- Đếm event theo lúc xảy ra, không theo lúc hệ thống nhận được · quiz · kit cho agent
- Đếm số người xem khác nhau mà không giữ hàng tỉ dòng · quiz
- Số click trên dashboard của người bán khác số trên hóa đơn · kit cho agent

### CQRS, event sourcing và khi nào CRUD là đủ

- Chọn CRUD kèm audit log, CQRS hay event sourcing cho từng service
- Kiểm tra expected version khi hai command cùng ghi vào một aggregate · quiz · kit cho agent
- Dùng snapshot để không phải đọc lại cả stream mỗi lần ghi event · quiz · kit cho agent
- Đổi schema của event khi event cũ trong event store không được sửa
