Chương 19. Phát Sự Kiện Cho Nhiều Bên Nghe: Pub/Sub

Ở chương 18, ta nói về Queue:

> Một công việc được đưa vào hàng đợi, một worker lấy ra xử lý.

Chương này nói về một kiểu giao tiếp khác:

> Một sự kiện xảy ra, nhiều bên cùng có thể nghe và phản ứng.

Đó là Pub/Sub.

Pub/Sub là viết tắt của:

  • Publish: phát đi.
  • Subscribe: đăng ký nghe.

Ví dụ:

Ordering phát OrderPlaced

Kitchen nghe OrderPlaced để tạo phiếu làm bánh
Notification nghe OrderPlaced để gửi thông báo
Analytics nghe OrderPlaced để ghi nhận conversion
Inventory nghe OrderPlaced để giữ nguyên liệu

Điểm quan trọng:

> Ordering không cần biết cụ thể có bao nhiêu bên đang nghe.

Ordering chỉ nói:

OrderPlaced đã xảy ra.

Các bên khác tự quyết định:

Tôi có quan tâm không?
Nếu có, tôi phản ứng thế nào?

Đây là sức mạnh của Pub/Sub.

Nhưng cũng giống Queue, Pub/Sub không phải phép màu.

Nếu dùng sai, hệ thống sẽ khó debug, event mất, xử lý trùng, thứ tự lộn, hoặc nghiệp vụ bị giấu trong một đám handler khó theo dõi.

---

19.1. Pub/Sub là gì?

Pub/Sub là mô hình giao tiếp trong đó producer không gửi message trực tiếp đến từng consumer.

Producer publish message vào một kênh, topic hoặc event bus.

Consumer nào subscribe thì nhận message.

Mô hình:

Publisher -> Topic/Event Bus -> Subscribers

Ví dụ:

Payment Service
-> publish PaymentSucceeded

Subscribers:
- Ordering Service
- Accounting Service
- Notification Service
- Analytics Service

Payment không cần gọi từng service:

Payment -> Ordering
Payment -> Accounting
Payment -> Notification
Payment -> Analytics

Nó chỉ phát sự kiện.

Các bên nghe tự xử lý.

---

19.2. Ví dụ quán bánh: loa thông báo trong quán

Hãy quay lại quán bánh.

Khi khách đặt bánh thành công, nhân viên quầy không cần chạy đi từng phòng:

Chạy vào bếp báo có đơn mới
Chạy sang thu ngân báo cần thanh toán
Chạy sang kho báo giữ nguyên liệu
Chạy sang marketing báo có conversion
Chạy sang chăm sóc khách hàng báo khách VIP vừa đặt

Thay vào đó, quán có một hệ thống thông báo nội bộ:

Đơn #123 đã được đặt.

Bếp nghe và tạo phiếu làm bánh.

Kho nghe và giữ nguyên liệu.

Thu ngân nghe nếu cần xử lý thanh toán.

Marketing nghe để ghi nhận thống kê.

Chăm sóc khách hàng nghe nếu đơn có ghi chú đặc biệt.

Người phát thông báo không cần biết tất cả ai đang nghe.

Đây là Pub/Sub.

---

19.3. Pub/Sub khác Queue ở đâu?

Queue và Pub/Sub rất dễ bị lẫn vì đều dùng message.

Khác biệt chính:

Queue:
  Một job thường được một worker xử lý.

Pub/Sub:
  Một event có thể được nhiều subscriber xử lý.

Ví dụ Queue:

RunGradingJob(job_123)

Chỉ một worker nên chấm job này.

Nếu hai worker cùng chấm một job và ghi hai kết quả, đó là lỗi.

Ví dụ Pub/Sub:

GradingCompleted(job_123)

Nhiều bên có thể cần biết:

  • Learning cập nhật tiến độ.
  • Notification báo học viên.
  • Analytics ghi chi phí.
  • Certificate kiểm tra điều kiện cấp chứng chỉ.

So sánh:

| Câu hỏi | Queue | Pub/Sub | |---|---|---| | Message đại diện cho gì? | Việc cần làm | Sự kiện đã xảy ra | | Bao nhiêu bên xử lý? | Thường một worker trong một nhóm | Nhiều subscriber độc lập | | Ví dụ | SendEmailJob | OrderPlaced | | Mục tiêu | Chia việc, xử lý sau | Phát sự kiện cho nhiều bên nghe | | Tư duy | "Ai làm việc này?" | "Ai quan tâm chuyện này?" |

Một số công cụ hỗ trợ cả hai kiểu, nhưng tư duy thiết kế vẫn khác.

---

19.4. Event trong Pub/Sub nên là sự thật đã xảy ra

Pub/Sub rất hợp với domain event.

Tên event nên ở thì quá khứ:

OrderPlaced
PaymentSucceeded
DeliveryFailed
GradingCompleted
RefundIssued
UserRegistered
SeatReserved

Không nên đặt event như command:

SendEmail
CreateInvoice
UpdateAnalytics

Vì đó là việc cần làm, không phải sự kiện đã xảy ra.

Event tốt nói:

> Điều gì đã xảy ra trong nghiệp vụ?

Subscriber tự quyết định:

> Tôi cần làm gì khi điều đó xảy ra?

Ví dụ:

OrderPlaced

Notification quyết định gửi email.

Kitchen quyết định tạo ticket.

Analytics quyết định ghi event.

CRM quyết định sync activity.

Ordering không cần biết những phản ứng đó.

---

19.5. Khi nào nên dùng Pub/Sub?

Pub/Sub phù hợp khi:

  • Một sự kiện có nhiều bên quan tâm.
  • Producer không nên biết hết consumer.
  • Muốn thêm phản ứng mới mà ít sửa producer.
  • Phản ứng có thể chạy độc lập.
  • Muốn giảm gọi trực tiếp giữa service/module.
  • Muốn xây read model, notification, analytics, integration.

Ví dụ:

OrderPlaced
-> Kitchen
-> Notification
-> Analytics
-> Inventory
PaymentSucceeded
-> Ordering
-> Accounting
-> Notification
-> Fulfillment
GradingCompleted
-> Learning
-> Notification
-> Certificate
-> Analytics

Nếu chỉ có một bên xử lý duy nhất và đó là một việc cụ thể, queue hoặc gọi trực tiếp có thể đơn giản hơn.

---

19.6. Khi nào chưa nên dùng Pub/Sub?

Chưa nên dùng Pub/Sub nếu:

  • Luồng rất đơn giản.
  • Chỉ có một consumer.
  • Cần kết quả ngay trong request chính.
  • Chưa có monitoring/retry/idempotency.
  • Event chỉ dùng để gọi vòng vo một hàm.
  • Team chưa hiểu rõ domain event.

Ví dụ:

Admin sửa tên category.

Nếu không có bên nào cần phản ứng, không cần Pub/Sub.

Ví dụ:

User đăng nhập và cần token ngay.

Không dùng Pub/Sub để tạo token.

Ví dụ:

Một service cần hỏi trạng thái payment hiện tại.

Dùng HTTP/query, không dùng Pub/Sub.

Pub/Sub phù hợp để thông báo chuyện đã xảy ra, không phù hợp để hỏi dữ liệu ngay.

---

19.7. Pub/Sub bền và Pub/Sub tạm thời

Đây là ranh giới cực kỳ quan trọng.

Không phải Pub/Sub nào cũng bền.

Có hai kiểu lớn:

Pub/Sub bền

Message được lưu đủ lâu để consumer có thể xử lý lại nếu đang offline hoặc lỗi.

Ví dụ:

  • Kafka.
  • Google Pub/Sub.
  • AWS SNS kết hợp SQS.
  • RabbitMQ exchange với durable queue.
  • NATS JetStream.
  • Redis Streams.

Phù hợp cho event nghiệp vụ quan trọng:

PaymentSucceeded
OrderPlaced
GradingCompleted
RefundIssued

Pub/Sub tạm thời

Message chỉ đến những subscriber đang online.

Nếu subscriber không online, message có thể mất.

Ví dụ:

  • Redis Pub/Sub truyền thống.
  • In-memory event bus.
  • WebSocket broadcast tạm thời.

Phù hợp cho realtime signal không quá quan trọng:

User is typing
Online status changed
Live dashboard tick
Transient notification badge

Không phù hợp cho event quan trọng như payment thành công.

Thông điệp chính:

> Đừng dùng Pub/Sub không bền cho sự kiện nghiệp vụ không được phép mất.

---

19.8. Vì sao Pub/Sub không bền nguy hiểm cho job quan trọng?

Giả sử Payment phát:

PaymentSucceeded

qua một kênh Pub/Sub không bền.

Đúng lúc đó Accounting service restart.

Accounting không nhận được event.

Kết quả:

  • Payment đã thành công.
  • Order có thể đã paid.
  • Nhưng Accounting không ghi doanh thu.

Đây là lỗi nghiêm trọng.

Nếu dùng Pub/Sub không bền cho event quan trọng, bạn cần chấp nhận:

Subscriber offline = message có thể mất

Với nghiệp vụ quan trọng, cần:

  • Durable subscription.
  • Message persistence.
  • Retry.
  • DLQ.
  • Replay hoặc reconciliation.
  • Outbox ở producer.

Redis Pub/Sub kiểu tạm thời có thể rất tiện cho realtime notification, nhưng không nên làm xương sống nghiệp vụ quan trọng.

---

19.9. Topic là gì?

Topic là kênh mà publisher gửi message vào.

Ví dụ:

orders.events
payments.events
grading.events
notifications.events

Hoặc theo event type:

OrderPlaced
PaymentSucceeded
GradingCompleted

Consumer subscribe topic để nhận event.

Thiết kế topic ảnh hưởng đến:

  • Dễ tìm event.
  • Dễ phân quyền.
  • Dễ scale.
  • Dễ replay.
  • Dễ tách consumer.

Không có cách đặt topic duy nhất đúng.

Nhưng cần tránh hai cực đoan:

Một topic chứa mọi thứ

quá khó quản lý.

Mỗi event một topic cực nhỏ

có thể quá vụn nếu hệ thống chưa cần.

Cách thực dụng thường là nhóm theo domain/context:

ordering.events
payment.events
grading.events
delivery.events

Trong message có event_type.

---

19.10. Subscriber là gì?

Subscriber là bên đăng ký nghe event.

Ví dụ:

Notification subscriber nghe OrderPlaced
Analytics subscriber nghe OrderPlaced
Kitchen subscriber nghe OrderPlaced

Mỗi subscriber nên có trách nhiệm rõ.

Không nên có một subscriber khổng lồ:

OrderEventSubscriber
  send email
  create kitchen ticket
  update analytics
  sync CRM
  reserve inventory

Cách tốt hơn:

CreateKitchenTicketOnOrderPlaced
SendConfirmationOnOrderPlaced
TrackAnalyticsOnOrderPlaced
SyncCrmOnOrderPlaced
ReserveInventoryOnOrderPlaced

Mỗi handler làm một việc.

Lỗi ở analytics không nên làm hỏng notification.

Lỗi ở email không nên làm hỏng kitchen ticket.

---

19.11. Consumer group là gì?

Trong một số hệ thống, mỗi subscriber có thể là một nhóm nhiều worker.

Ví dụ:

Topic: grading.events

Consumer group: notification-service
  worker 1
  worker 2
  worker 3

Consumer group: analytics-service
  worker 1
  worker 2

Ý nghĩa:

  • Mỗi consumer group nhận một bản của event.
  • Các worker trong cùng group chia nhau xử lý.

Ví dụ GradingCompleted:

Notification service nhận event một lần, nhưng có thể worker 1 xử lý.

Analytics service cũng nhận event một lần, có thể worker 2 xử lý.

Như vậy:

Một event -> nhiều consumer group
Trong mỗi group -> một worker xử lý message đó

Đây là cách Pub/Sub kết hợp với queue-like scaling.

---

19.12. Pub/Sub và Domain Event

Domain event là ý nghĩa nghiệp vụ.

Pub/Sub là cách phát event đó đến nhiều bên.

Ví dụ domain event:

OrderPlaced

Cách truyền:

In-memory event bus trong monolith
RabbitMQ exchange
Kafka topic
Google Pub/Sub topic
SNS topic
Redis Streams

Đừng nhầm:

> Domain event là cái được nói. Pub/Sub là cái loa.

Cùng một event có thể bắt đầu bằng in-memory trong monolith.

Sau này khi tách service, nó có thể đi qua message broker.

Nếu event được thiết kế rõ từ đầu, việc đổi hạ tầng dễ hơn.

---

19.13. Pub/Sub trong monolith

Monolith vẫn dùng Pub/Sub được, nhưng thường ở dạng nhẹ.

Ví dụ:

PlaceOrderUseCase
-> OrderPlaced
-> in-process handlers:
   - create kitchen ticket
   - send notification job
   - track analytics

Trong monolith nhỏ, handler có thể chạy đồng bộ.

Nhưng nếu handler chậm hoặc dễ lỗi, nên chuyển sang outbox/queue.

Pub/Sub trong monolith giúp:

  • Tách use case chính khỏi phản ứng phụ.
  • Module ít gọi trực tiếp nhau hơn.
  • Sau này tách service dễ hơn.

Nhưng không nên làm quá:

  • Nếu luồng chỉ có một bước, gọi function trực tiếp ổn.
  • Nếu event chỉ để né cấu trúc code rõ ràng, sẽ khó đọc hơn.

---

19.14. Pub/Sub trong microservices

Trong microservices, Pub/Sub thường rất hữu ích vì service không nên biết hết nhau.

Ví dụ:

payment-service
-> publish PaymentSucceeded

ordering-service subscribes
accounting-service subscribes
notification-service subscribes
analytics-service subscribes

Payment service không gọi trực tiếp từng bên.

Nó chỉ công bố sự thật:

PaymentSucceeded

Điều này giúp:

  • Thêm consumer mới dễ hơn.
  • Giảm coupling trực tiếp.
  • Service deploy độc lập hơn.
  • Phản ứng phụ không kéo chậm producer.

Nhưng cũng tạo vấn đề:

  • Eventual consistency.
  • Debug khó hơn.
  • Cần schema/version.
  • Cần xử lý duplicate.
  • Cần monitoring.
  • Cần replay/reconciliation.

Microservices dùng Pub/Sub mà không có observability sẽ rất vất vả.

---

19.15. Pub/Sub và event-driven architecture

Event-driven architecture là kiến trúc trong đó nhiều phần hệ thống phản ứng với event.

Pub/Sub là một công cụ phổ biến để làm điều đó.

Ví dụ:

OrderPlaced
-> PaymentRequested
-> PaymentSucceeded
-> KitchenTicketCreated
-> DeliveryTaskCreated
-> OrderDelivered

Mỗi event mở ra phản ứng tiếp theo.

Điểm tốt:

  • Hệ thống linh hoạt.
  • Phản ứng mới dễ thêm.
  • Service ít gọi trực tiếp.
  • Việc lâu có thể xử lý nền.

Điểm khó:

  • Luồng nghiệp vụ không nằm trong một file.
  • Cần trace để hiểu chuyện gì xảy ra.
  • Cần xử lý event trùng, event muộn, event lỗi.
  • Cần biết ai sở hữu state cuối.

Event-driven không nên là excuse để mất kiểm soát luồng.

Cần context map, event catalog, và monitoring.

---

19.16. Event catalog là gì?

Event catalog là danh sách các event quan trọng trong hệ thống.

Ví dụ:

Ordering Events:
- OrderPlaced
- OrderCancelled
- OrderConfirmed

Payment Events:
- PaymentSucceeded
- PaymentFailed
- RefundIssued

Grading Events:
- SubmissionCreated
- GradingJobStarted
- GradingCompleted
- GradingFailed

Mỗi event nên có:

  • Ý nghĩa nghiệp vụ.
  • Producer.
  • Consumers.
  • Payload schema.
  • Version.
  • Khi nào phát.
  • Idempotency key hoặc event_id.
  • Chính sách retry/DLQ nếu có.

Event catalog không cần công cụ phức tạp lúc đầu.

Một file markdown cũng đủ.

Quan trọng là team biết:

> Hệ thống đang nói những sự kiện gì, ai phát, ai nghe, và vì sao.

---

19.17. Event schema

Event là contract.

Schema giúp consumer hiểu event.

Ví dụ:

{
  "event_id": "evt_123",
  "event_type": "OrderPlaced",
  "schema_version": 1,
  "occurred_at": "2026-05-11T10:00:00Z",
  "aggregate_id": "order_456",
  "correlation_id": "req_789",
  "data": {
    "order_id": "order_456",
    "customer_id": "cust_001",
    "total_amount": 350000,
    "currency": "VND"
  }
}

Schema nên làm rõ:

  • Event loại gì?
  • Xảy ra lúc nào?
  • Xảy ra với đối tượng nào?
  • Dữ liệu nghiệp vụ là gì?
  • Version nào?
  • Trace theo luồng nào?

Không có schema, consumer sẽ đoán.

Mà trong hệ thống lớn, đoán là nguồn gốc của rất nhiều lỗi.

---

19.18. Event versioning

Khi event đã có consumer, đổi event giống như đổi API.

Thêm field thường an toàn hơn:

v1:
- order_id
- total_amount

v2:
- order_id
- total_amount
- currency

Xóa field hoặc đổi nghĩa field nguy hiểm hơn.

Ví dụ field amount ban đầu là VND, sau đổi thành cents mà không báo, consumer sẽ tính sai.

Quy tắc:

  • Không đổi nghĩa field âm thầm.
  • Không xóa field khi consumer cũ còn dùng.
  • schema_version.
  • Consumer nên bỏ qua field lạ.
  • Producer nên giữ backward compatibility khi có thể.
  • Với thay đổi lớn, tạo event version mới.

Event cũ có thể vẫn nằm trong broker hoặc log.

Worker mới phải biết xử lý event cũ hoặc bỏ qua an toàn.

---

19.19. Pub/Sub và outbox

Nếu một service thay đổi database rồi publish event, có rủi ro:

save payment succeeded
publish PaymentSucceeded failed

Payment đã thành công nhưng không ai biết.

Hoặc:

publish PaymentSucceeded
save payment failed

Consumer nhận event về một payment không tồn tại.

Outbox giải quyết bằng cách:

begin transaction
  save payment state
  save PaymentSucceeded vào outbox
commit

dispatcher publish event từ outbox

Outbox giúp event và state chính nhất quán.

Nhưng outbox có thể publish trùng nếu dispatcher không chắc đã mark published.

Vì vậy consumer vẫn cần idempotency.

Outbox không thay thế idempotency.

Hai thứ đi cùng nhau.

---

19.20. Idempotency trong Pub/Sub

Consumer có thể nhận cùng event nhiều lần.

Lý do:

  • Broker retry.
  • Consumer xử lý xong nhưng ack thất bại.
  • Outbox publish lại.
  • Network lỗi.
  • Replay event cũ.

Vì vậy handler phải idempotent.

Ví dụ:

OrderPlaced -> CreateKitchenTicket

Nếu nhận OrderPlaced hai lần, không được tạo hai kitchen ticket cho cùng order.

Cách xử lý:

  • Unique constraint theo order_id.
  • Lưu event_id đã xử lý.
  • Kiểm tra state trước khi tạo.
  • Dùng idempotency key.

Ví dụ:

unique(kitchen_ticket.order_id)

Nếu handler chạy lại, insert lần hai fail có kiểm soát hoặc bỏ qua.

Trong Pub/Sub, hãy luôn giả định:

> Event có thể đến hơn một lần.

---

19.21. Ordering trong Pub/Sub

Event có thể đến sai thứ tự.

Ví dụ:

OrderPlaced
OrderCancelled

Consumer có thể nhận OrderCancelled trước OrderPlaced, tùy broker, partition, retry, và consumer lag.

Hoặc:

PaymentSucceeded
RefundIssued

Nếu RefundIssued đến trước PaymentSucceeded, Accounting xử lý thế nào?

Cách giảm rủi ro:

  • Partition theo aggregate_id nếu broker hỗ trợ.
  • Dùng sequence/version.
  • Handler kiểm tra state hiện tại.
  • Nếu thiếu event trước, retry sau.
  • Có reconciliation job.

Không phải mọi event cần strict ordering.

Analytics thường chịu được lệch.

Wallet/payment/accounting có thể cần cẩn thận hơn.

Đừng giả định Pub/Sub tự đảm bảo đúng thứ tự toàn hệ thống.

---

19.22. Eventual consistency

Pub/Sub thường dẫn đến eventual consistency.

Ví dụ:

Payment = SUCCEEDED
Order vẫn = WAITING_PAYMENT trong 1 giây

Sau khi Ordering consumer xử lý PaymentSucceeded:

Order = PAID

Trong thời gian ngắn, dữ liệu giữa service có thể lệch.

Điều này có thể chấp nhận nếu:

  • UI hiển thị đúng trạng thái đang cập nhật.
  • Consumer retry khi lỗi.
  • Có monitoring backlog.
  • Có reconciliation nếu lệch quá lâu.
  • Biết nguồn sự thật là ai.

Eventual consistency không có nghĩa là dữ liệu muốn lệch bao lâu cũng được.

Nó nghĩa là hệ thống chấp nhận lệch tạm thời có kiểm soát.

---

19.23. Pub/Sub và realtime notification

Pub/Sub cũng hay dùng cho realtime notification.

Ví dụ:

GradingCompleted
-> Notification service
-> publish realtime notification
-> WebSocket/SSE gửi đến frontend

Hoặc:

ChatMessageCreated
-> broadcast đến người trong phòng chat

Ở đây cần phân biệt:

Event nghiệp vụ bền

GradingCompleted

Không được mất.

Nên đi qua durable event/message system.

Realtime signal tạm thời

send toast to connected browser

Nếu user offline, có thể không nhận realtime signal.

Nhưng notification vẫn nên lưu trong database để user mở lại còn thấy.

Thiết kế tốt:

GradingCompleted
-> tạo Notification record trong DB
-> nếu user đang online, gửi realtime signal

Đừng chỉ gửi Pub/Sub tạm thời đến WebSocket rồi không lưu gì.

User offline là mất thông báo.

---

19.24. Pub/Sub backend cho WebSocket

Khi có nhiều WebSocket server, cần cách broadcast giữa chúng.

Ví dụ:

User A connected to ws-server-1
User B connected to ws-server-2

Nếu có message cần gửi cho User B nhưng logic chạy ở server 1, server 1 phải báo server 2.

Pub/Sub backend giúp:

App publish notification:user_123
ws-server nào giữ connection của user_123 thì gửi ra socket

Công cụ thường gặp:

  • Redis Pub/Sub.
  • Redis Streams.
  • NATS.
  • Kafka.
  • Managed pub/sub.

Với realtime signal tạm thời, Redis Pub/Sub có thể ổn.

Với notification quan trọng, phải lưu DB/event bền trước.

---

19.25. Pub/Sub và analytics

Analytics rất hợp với Pub/Sub.

Ví dụ:

OrderPlaced
PaymentSucceeded
GradingCompleted
LessonCompleted
SearchPerformed

Analytics consumer nghe event và ghi vào kho dữ liệu.

Producer không cần gọi trực tiếp analytics trên đường request chính.

Nếu analytics chậm hoặc lỗi, nghiệp vụ chính vẫn chạy.

Nhưng analytics cũng cần:

  • Idempotency hoặc dedup.
  • Schema rõ.
  • Event timestamp.
  • User/session id nếu được phép.
  • Privacy policy.
  • Backfill/replay nếu cần.

Analytics có thể chịu eventual consistency tốt hơn payment/order.

Vì vậy nó là consumer rất phù hợp của Pub/Sub.

---

19.26. Pub/Sub và search index

Search index thường được cập nhật từ event.

Ví dụ:

ProductPublished
ProductUpdated
ProductDeleted

Search service nghe event và cập nhật Elasticsearch/OpenSearch/Meilisearch.

Điểm cần hiểu:

> Search index thường là read model, không phải nguồn sự thật.

Nguồn sự thật có thể là Catalog database.

Nếu search index chậm vài giây, thường chấp nhận được.

Nếu index bị lỗi, có thể rebuild từ database hoặc replay event.

Không nên để việc cập nhật search index làm request cập nhật sản phẩm thất bại, trừ khi nghiệp vụ yêu cầu rất đặc biệt.

---

19.27. Pub/Sub và cache invalidation

Event có thể dùng để invalidate cache.

Ví dụ:

ProductUpdated
-> Cache service xóa cache product_123

Hoặc:

UserRoleChanged
-> xóa permission cache của user

Cache invalidation cần cẩn thận:

  • Nếu event mất, cache có thể stale lâu.
  • Nếu event đến trễ, cache stale tạm thời.
  • Nếu event trùng, xóa cache nhiều lần thường không sao.

Với dữ liệu bảo mật như permission, nên thiết kế chặt hơn:

  • TTL ngắn.
  • Version permission.
  • Recheck ở nguồn sự thật khi cần.

Pub/Sub giúp cache biết khi nào cần xóa, nhưng không thay thế tư duy consistency.

---

19.28. Pub/Sub và integration với hệ thống ngoài

Khi một sự kiện xảy ra, có thể cần đồng bộ sang hệ thống ngoài:

  • CRM.
  • ERP.
  • Email marketing.
  • Data warehouse.
  • Partner API.

Ví dụ:

CustomerRegistered
-> SyncCustomerToCrm

Integration thường:

  • Chậm.
  • Dễ lỗi.
  • Có rate limit.
  • Không nên nằm trên request chính.

Pub/Sub + queue/worker rất phù hợp.

Nhưng cần:

  • Retry có kiểm soát.
  • DLQ.
  • Idempotency với hệ thống ngoài.
  • Mapping dữ liệu rõ.
  • Log raw response nếu cần debug.

Đừng để CRM down làm khách không đăng ký được tài khoản, trừ khi CRM thật sự là dependency bắt buộc.

---

19.29. Pub/Sub và choreography

Trong hệ thống event-driven, có hai cách điều phối luồng:

  • Orchestration.
  • Choreography.

Orchestration:

> Có một người điều phối trung tâm gọi từng bước.

Choreography:

> Mỗi service nghe event và tự phản ứng, không có trung tâm điều phối rõ.

Ví dụ choreography:

OrderPlaced
-> Payment creates payment
-> PaymentSucceeded
-> Kitchen creates ticket
-> KitchenTicketCreated
-> Delivery prepares delivery

Ưu điểm:

  • Ít coupling trực tiếp.
  • Service tự chủ hơn.

Nhược điểm:

  • Khó nhìn toàn bộ luồng.
  • Debug khó.
  • Dễ tạo vòng lặp event.
  • Khó biết trạng thái tổng.

Với luồng nghiệp vụ dài và quan trọng, đôi khi nên có workflow/saga orchestrator rõ.

Pub/Sub tốt, nhưng không phải luồng nào cũng nên choreography hoàn toàn.

---

19.30. Vòng lặp event

Một lỗi nguy hiểm là event loop.

Ví dụ:

OrderUpdated
-> Payment consumer cập nhật payment
-> PaymentUpdated
-> Order consumer cập nhật order
-> OrderUpdated
-> ...

Hệ thống tự kích hoạt qua lại.

Cách tránh:

  • Event name cụ thể, tránh Updated chung chung.
  • Handler kiểm tra thay đổi có thật cần phát event không.
  • Có causation_id/correlation_id.
  • Không phát event nếu state không đổi.
  • Thiết kế ownership rõ.

Event quá chung như DataChanged, StatusUpdated rất dễ gây vòng lặp hoặc consumer hiểu sai.

---

19.31. Pub/Sub và quyền sở hữu dữ liệu

Pub/Sub không có nghĩa là ai cũng được sửa dữ liệu của ai.

Ví dụ:

Payment phát:

PaymentSucceeded

Ordering nghe và cập nhật Order theo luật của Ordering.

Nhưng Accounting không nên sửa database của Payment.

Notification không nên sửa Order trực tiếp.

Event giúp các context biết chuyện đã xảy ra, nhưng mỗi context vẫn sở hữu dữ liệu của mình.

Nguyên tắc:

> Nghe event để cập nhật state của mình, không phải để chọc vào state của người khác.

Nếu consumer cần thay đổi dữ liệu của context khác, nên gửi command/API phù hợp hoặc để context đó tự nghe event và quyết định.

---

19.32. Pub/Sub và bảo mật dữ liệu

Event có thể đi đến nhiều consumer.

Vì vậy không nên đưa dữ liệu nhạy cảm vào event nếu không cần.

Tránh:

  • Password.
  • Token.
  • Secret.
  • Full card number.
  • Dữ liệu cá nhân quá chi tiết.
  • Nội dung riêng tư không cần thiết.

Ví dụ PaymentSucceeded không cần chứa raw payment gateway payload đầy đủ.

Có thể chỉ cần:

payment_id
order_id
amount
currency
paid_at
method_type

Ngoài ra cần kiểm soát:

  • Service nào được subscribe topic nào.
  • Event được lưu bao lâu.
  • Log có chứa payload nhạy cảm không.
  • Có cần mã hóa không.
  • Có cần redact không.

Event lan rộng hơn API call trực tiếp, nên phải nghĩ về dữ liệu từ đầu.

---

19.33. Pub/Sub và replay

Replay là xử lý lại event cũ.

Rất hữu ích khi:

  • Build lại search index.
  • Tính lại analytics.
  • Consumer mới cần dữ liệu lịch sử.
  • Sửa bug consumer rồi chạy lại.

Nhưng replay nguy hiểm với side effect thật.

Ví dụ replay OrderPlaced không nên gửi lại email xác nhận đơn cũ cho toàn bộ khách.

Vì vậy cần phân biệt handler:

  • Handler build read model: replay thường ổn.
  • Handler analytics: replay thường ổn nếu dedup đúng.
  • Handler gửi email/SMS: replay phải rất cẩn thận.
  • Handler gọi payment/refund: cực kỳ cẩn thận.

Không phải event nào cũng được replay vào mọi consumer.

Replay là sức mạnh lớn, nhưng phải có hàng rào.

---

19.34. Pub/Sub và reconciliation

Reconciliation là kiểm tra và sửa lệch dữ liệu định kỳ.

Vì event có thể trễ, lỗi, hoặc consumer fail, hệ thống quan trọng thường cần job đối soát.

Ví dụ:

Payment = SUCCEEDED
Order chưa PAID sau 10 phút
-> reconciliation phát hiện
-> sửa hoặc enqueue lại event

Hoặc:

GradingJob = SUCCEEDED
LearningProgress chưa cập nhật
-> reconciliation cập nhật lại

Pub/Sub tốt giảm coupling, nhưng không loại bỏ nhu cầu kiểm tra tính đúng lâu dài.

Đặc biệt với payment, accounting, access control, entitlement, cần đối soát.

---

19.35. Pub/Sub và monitoring

Pub/Sub cần observability tốt.

Cần theo dõi:

  • Publish rate.
  • Consumer lag.
  • Error rate của từng consumer.
  • Retry count.
  • DLQ count.
  • Oldest unprocessed message.
  • Event processing latency.
  • Message size.
  • Broker health.

Cần log:

  • event_id.
  • event_type.
  • producer.
  • consumer.
  • attempt.
  • correlation_id.
  • processing result.

Khi user hỏi:

> Tôi thanh toán rồi sao đơn chưa paid?

Ta cần trả lời được:

PaymentSucceeded có được publish không?
Ordering consumer có nhận không?
Consumer lỗi gì?
Message đang lag hay vào DLQ?
Order update có conflict không?

Nếu không có monitoring, Pub/Sub trở thành hộp đen.

---

19.36. Consumer lag

Consumer lag là độ trễ giữa event được publish và consumer xử lý xong.

Ví dụ:

PaymentSucceeded publish lúc 10:00
Ordering xử lý lúc 10:05
lag = 5 phút

Lag cao có thể do:

  • Consumer ít worker.
  • Consumer lỗi và retry.
  • Message quá nhiều.
  • Broker chậm.
  • Downstream database/API chậm.

Lag không phải lúc nào cũng xấu.

Analytics lag 5 phút có thể ổn.

Payment/order lag 5 phút có thể rất tệ.

Mỗi consumer cần kỳ vọng riêng.

Đừng chỉ nhìn topic chung.

Hãy nhìn lag theo consumer.

---

19.37. DLQ trong Pub/Sub

DLQ cũng quan trọng trong Pub/Sub.

Nếu consumer xử lý event lỗi nhiều lần, message nên vào DLQ.

Ví dụ:

Accounting consumer xử lý PaymentSucceeded
-> lỗi do missing tax config
-> retry 5 lần
-> vào DLQ

DLQ giúp:

  • Không chặn consumer mãi.
  • Có nơi debug lỗi.
  • Có thể sửa rồi replay.
  • Có alert cho team sở hữu consumer.

Mỗi consumer có thể có DLQ riêng.

Vì cùng một event, Notification có thể xử lý thành công, nhưng Accounting fail.

Đây là điểm khác với gọi trực tiếp: mỗi subscriber có đời sống lỗi riêng.

---

19.38. Pub/Sub và backpressure

Nếu consumer xử lý không kịp, lag tăng.

Producer vẫn publish đều.

Câu hỏi:

> Khi consumer chậm, producer có cần chậm lại không?

Tùy hệ thống.

Với analytics, producer thường không chờ.

Với hệ thống quan trọng, nếu lag quá cao có thể cần:

  • Scale consumer.
  • Tạm dừng producer phụ.
  • Giảm event không quan trọng.
  • Tách topic.
  • Áp dụng rate limit.
  • Bật degradation mode.

Pub/Sub thường giảm coupling thời gian, nhưng không có nghĩa tài nguyên vô hạn.

Nếu consumer mãi không xử lý kịp, dữ liệu downstream sẽ lỗi thời.

---

19.39. Pub/Sub và schema registry

Khi event nhiều và consumer nhiều, có thể cần schema registry.

Schema registry là nơi quản lý schema event:

  • Event type.
  • Version.
  • Field.
  • Compatibility rule.

Công cụ như Kafka ecosystem thường dùng schema registry với Avro/Protobuf/JSON Schema.

Không cần bắt đầu quá nặng.

Nhưng khi team lớn, schema registry giúp:

  • Producer không publish schema sai.
  • Consumer biết schema nào đang dùng.
  • Kiểm tra compatibility trước deploy.
  • Quản lý version tốt hơn.

Với hệ thống nhỏ, một file markdown hoặc OpenAPI-like JSON schema cũng đã là bước tiến lớn.

---

19.40. Pub/Sub và công cụ phổ biến

Một số công cụ:

RabbitMQ exchange
Kafka topics
Google Pub/Sub
AWS SNS + SQS
Redis Pub/Sub
Redis Streams
NATS / NATS JetStream
Azure Event Grid / Service Bus
Pusher/Ably cho realtime

Không chọn công cụ chỉ vì nổi tiếng.

Hỏi:

  • Message có cần bền không?
  • Có cần replay không?
  • Có cần ordering theo key không?
  • Throughput bao nhiêu?
  • Consumer có cần group không?
  • Có cần DLQ không?
  • Team vận hành được không?
  • Cloud đang dùng gì?

Ví dụ:

  • Redis Pub/Sub tốt cho realtime signal tạm thời.
  • RabbitMQ tốt cho routing/queue truyền thống.
  • Kafka tốt cho event log/streaming/replay lớn.
  • Google Pub/Sub/SNS/SQS tốt nếu muốn managed cloud.

---

19.41. Redis Pub/Sub dùng khi nào?

Redis Pub/Sub rất đơn giản và nhanh.

Phù hợp cho:

  • Realtime broadcast tạm thời.
  • In-app notification signal khi đã lưu DB.
  • WebSocket fanout nội bộ.
  • Cache invalidation không quá nghiêm trọng, nếu có TTL/reconcile.

Không phù hợp cho:

  • PaymentSucceeded quan trọng.
  • OrderPlaced không được mất.
  • Job cần retry chắc chắn.
  • Event cần replay.
  • Consumer offline vẫn phải nhận.

Vì Redis Pub/Sub truyền thống không lưu message cho subscriber offline.

Nếu cần bền hơn trong Redis ecosystem, có thể xem Redis Streams.

Nhưng vẫn phải hiểu rõ cơ chế consumer group, ack, retry, pending entries.

---

19.42. AWS SNS + SQS dễ hiểu

Một pattern phổ biến:

SNS topic -> nhiều SQS queue -> mỗi service xử lý queue riêng

Ví dụ:

PaymentSucceeded SNS topic
-> ordering-payment-events SQS
-> accounting-payment-events SQS
-> notification-payment-events SQS

Mỗi service có queue riêng.

Nếu Notification lỗi, queue của Notification backlog.

Ordering vẫn xử lý queue của nó bình thường.

Đây là cách rất rõ để làm Pub/Sub bền:

  • Topic để fanout.
  • Queue riêng để mỗi consumer xử lý độc lập.
  • DLQ riêng cho từng consumer.

Tư duy này cũng áp dụng được với nhiều công cụ khác.

---

19.43. Pub/Sub và AI Judge

AI Judge có nhiều event phù hợp Pub/Sub.

Ví dụ:

SubmissionCreated
GradingJobStarted
GradingCompleted
GradingFailed

Khi SubmissionCreated:

  • Grading tạo job.
  • Analytics ghi hành vi nộp bài.
  • Notification có thể báo đã nhận bài.

Khi GradingCompleted:

  • Learning cập nhật tiến độ.
  • Notification báo học viên.
  • Certificate kiểm tra điều kiện.
  • Analytics ghi score, latency, cost.

Luồng:

RunGradingJobUseCase
-> GradingJob complete
-> save GradingCompleted to outbox
-> publish grading.events

Subscribers:
- LearningProgressOnGradingCompleted
- NotifyStudentOnGradingCompleted
- TrackAiCostOnGradingCompleted
- CheckCertificateOnGradingCompleted

Điểm quan trọng:

AI worker không nên tự làm hết:

chấm bài
cập nhật learning
gửi notification
tính certificate
ghi analytics

Nó nên hoàn thành việc chấm và phát sự kiện.

Các phần khác phản ứng.

---

19.44. Pub/Sub và notification

Notification thường có hai lớp:

Lớp sự kiện nghiệp vụ

GradingCompleted
PaymentSucceeded
DeliveryFailed

Không được mất.

Lớp gửi thông báo

CreateNotificationRecord
SendEmail
SendPush
SendRealtimeSignal

Cách tốt:

GradingCompleted
-> Notification service tạo notification trong DB
-> enqueue SendEmail/Push nếu cần
-> publish realtime signal nếu user online

Nếu realtime signal mất vì user offline, vẫn còn notification trong DB.

User mở app sau vẫn thấy.

Đừng chỉ dựa vào WebSocket Pub/Sub tạm thời cho thông báo quan trọng.

---

19.45. Pub/Sub có làm hệ thống dễ hơn không?

Ở mức nhỏ, không.

Pub/Sub thường làm hệ thống phức tạp hơn.

Bạn phải hiểu:

  • Event.
  • Handler.
  • Retry.
  • Duplicate.
  • Lag.
  • DLQ.
  • Schema.
  • Monitoring.

Nhưng ở hệ thống đủ lớn, Pub/Sub làm một kiểu phức tạp khác trở nên dễ chịu hơn:

Producer không phải biết và gọi trực tiếp mọi consumer.

Nó đổi:

Coupling trực tiếp

thành:

Coupling qua event contract

Đây là trade-off.

Không miễn phí, nhưng rất đáng giá khi có nhiều consumer độc lập.

---

19.46. Những lỗi phổ biến khi dùng Pub/Sub

Lỗi 1: Dùng Pub/Sub không bền cho event quan trọng

Subscriber offline là mất event.

Lỗi 2: Event tên quá chung

DataUpdated
StatusChanged
EntitySaved

Consumer không hiểu nghiệp vụ.

Lỗi 3: Consumer không idempotent

Event retry gây tạo trùng dữ liệu.

Lỗi 4: Không có event catalog

Không ai biết event nào tồn tại, ai nghe, ai phát.

Lỗi 5: Không monitoring consumer lag

Event vẫn publish, nhưng consumer chậm 30 phút không ai biết.

Lỗi 6: Handler làm quá nhiều việc

Một handler ôm email, analytics, CRM, support, cache.

Lỗi 7: Không version event

Producer đổi payload, consumer vỡ.

Lỗi 8: Nhầm event với command

SendEmail được publish như event, làm nghĩa hệ thống rối.

Lỗi 9: Không có reconciliation

Event lỗi làm downstream lệch mãi.

Lỗi 10: Phát dữ liệu nhạy cảm quá rộng

Event đi nhiều nơi hơn dự đoán.

---

19.47. Checklist thiết kế Pub/Sub

Khi thiết kế Pub/Sub, hãy hỏi:

  • Event này là sự kiện nghiệp vụ gì?
  • Tên event có ở thì quá khứ không?
  • Producer là ai?
  • Consumer là ai?
  • Consumer có thật sự cần event này không?
  • Topic nào chứa event?
  • Event có cần bền không?
  • Nếu consumer offline, có được mất event không?
  • Payload gồm gì?
  • Có dữ liệu nhạy cảm không?
  • Schema version là gì?
  • Event được publish sau commit bằng cách nào?
  • Có outbox không?
  • Consumer có idempotent không?
  • Có cần ordering theo key không?
  • Nếu consumer fail thì retry/DLQ ra sao?
  • Có cần replay không?
  • Có cần reconciliation không?
  • Có monitoring lag không?
  • Có event catalog không?

Nếu chưa trả lời được, khoan dùng Pub/Sub cho nghiệp vụ quan trọng.

---

19.48. Bảng chọn nhanh

| Tình huống | Pub/Sub có phù hợp không? | |---|---| | OrderPlaced nhiều bên cần biết | Rất phù hợp | | PaymentSucceeded cho order/accounting/notification | Rất phù hợp, phải bền | | User login cần token ngay | Không phù hợp | | Gửi realtime typing indicator | Phù hợp với Pub/Sub tạm thời | | Chấm một grading job cụ thể | Queue phù hợp hơn | | GradingCompleted nhiều bên phản ứng | Pub/Sub phù hợp | | Analytics nghe hành vi người dùng | Phù hợp | | Search index cập nhật theo ProductUpdated | Phù hợp | | Reset password email | Queue job phù hợp hơn, event có thể là trigger | | Payment/refund | Event phù hợp nhưng phải bền, idempotent, có đối soát |

---

19.49. Tóm tắt bằng một luồng

Luồng AI Judge:

Student submits assignment
-> SubmissionCreated
-> Grading creates GradingJob
-> Queue runs RunGradingJob
-> GradingCompleted

Sau GradingCompleted:

Learning updates progress
Notification creates notification
Analytics records AI cost and latency
Certificate checks completion
Realtime service signals browser if online

Trong đó:

  • Queue dùng để chạy một job chấm bài.
  • Pub/Sub dùng để phát kết quả chấm cho nhiều bên nghe.
  • Notification quan trọng nên lưu DB.
  • Realtime signal có thể tạm thời.
  • Consumer phải idempotent.
  • Event quan trọng phải bền.

---

19.50. Kết luận của chương

Pub/Sub là mô hình phát sự kiện cho nhiều bên nghe.

Nó giúp hệ thống:

  • Giảm việc service gọi trực tiếp lẫn nhau.
  • Thêm consumer mới dễ hơn.
  • Tách use case chính khỏi phản ứng phụ.
  • Phù hợp với notification, analytics, search index, integration, read model.
  • Hỗ trợ kiến trúc event-driven.

Nhưng Pub/Sub cũng đòi hỏi kỷ luật:

  • Event phải có nghĩa nghiệp vụ.
  • Event quan trọng phải bền.
  • Consumer phải idempotent.
  • Schema/version phải rõ.
  • Cần monitoring lag, retry, DLQ.
  • Cần outbox để không mất event.
  • Cần phân biệt realtime signal tạm thời với event nghiệp vụ quan trọng.

Thông điệp quan trọng nhất:

> Queue là giao một việc cho worker. Pub/Sub là công bố một sự thật để nhiều bên tự phản ứng.

Ở chương tiếp theo, ta sẽ đi sang Event Streaming: khi event không chỉ là thông báo tức thời, mà trở thành dòng lịch sử có thể lưu, replay, phân tích và xây dựng read model ở quy mô lớn.