Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 22 — Event-driven architecture (EDA)

Mở đầu — vì sao bài này quan trọng

Hãy hình dung bạn đang xây dựng hệ thống cho một sàn thương mại điện tử Việt Nam. Khi khách hàng đặt một đơn hàng, có hàng loạt việc cần xảy ra: trừ kho, tính điểm thưởng, gửi email xác nhận, thông báo cho nhà bán, đẩy yêu cầu sang đơn vị vận chuyển, cập nhật báo cáo doanh thu. Theo cách lập trình truyền thống, dịch vụ "Đặt hàng" sẽ phải tự tay gọi từng dịch vụ kia: gọi kho, gọi điểm thưởng, gọi email, gọi vận chuyển... Mỗi khi thêm một tính năng mới — ví dụ phòng chống gian lận — bạn lại phải mở code của dịch vụ "Đặt hàng" ra sửa.

Càng nhiều dịch vụ phụ thuộc trực tiếp vào nhau, hệ thống càng giống một mớ dây điện rối. Một dịch vụ chết kéo theo dịch vụ khác chết. Một thay đổi nhỏ buộc deploy lại cả chuỗi. Đây chính là vấn đề mà Event-Driven Architecture (EDA) — kiến trúc hướng sự kiện — sinh ra để giải quyết.

Trong các bài trước của khóa học, chúng ta đã nói về workflow, orchestration, iPaaS và API-led integration. EDA là một mảnh ghép khác biệt và bổ sung: thay vì các dịch vụ "ra lệnh" cho nhau, chúng "thông báo cho nhau biết chuyện gì vừa xảy ra". Đây là cách kiến trúc của những hệ thống quy mô lớn nhất thế giới — từ Grab, Shopee đến các ngân hàng số — đang vận hành. Nắm vững EDA là một kỹ năng nền tảng cho bất kỳ ai làm về automation và digital transformation.

Khái niệm cốt lõi

Sự kiện (event) là gì?

Một event là một bản ghi bất biến mô tả "một việc gì đó đã xảy ra trong quá khứ". Hãy chú ý cách diễn đạt: event luôn ở thì quá khứ. Ví dụ: OrderPlaced (đơn hàng đã được đặt), PaymentConfirmed (thanh toán đã xác nhận), InventoryReserved (kho đã giữ hàng).

Điểm mấu chốt: event chỉ kể lại sự thật, nó không ra lệnh. Đây là khác biệt căn bản giữa event và command (mệnh lệnh). Command nói "hãy trừ kho đi" — nó kỳ vọng một hành động cụ thể từ một người nhận cụ thể. Event nói "đơn hàng vừa được đặt" — ai quan tâm thì tự xử lý, người phát không cần biết và không cần quan tâm.

Một event thường gồm: tên loại sự kiện, thời điểm xảy ra, một định danh (ví dụ order_id), và dữ liệu kèm theo (payload). Ví dụ dạng JSON:

{
  "event_type": "OrderPlaced",
  "event_id": "evt_8f3a21",
  "occurred_at": "2026-06-27T10:15:00+07:00",
  "data": {
    "order_id": "ORD-2026-00123",
    "customer_id": "CUS-4521",
    "total_amount": 459000,
    "currency": "VND"
  }
}

Ba vai trò: Producer — Broker — Consumer

Đây là bộ ba xương sống của mọi hệ thống EDA, đúng như ghi chú gốc của bài:

Producer (bên phát) là dịch vụ tạo ra event khi có việc xảy ra. Trong ví dụ sàn TMĐT, dịch vụ "Đặt hàng" là producer phát ra OrderPlaced. Điều quan trọng: producer phát xong là xong, nó không cần biết có bao nhiêu bên đang lắng nghe, cũng không chờ ai phản hồi.

Broker (bên trung gian) là hạ tầng tiếp nhận, lưu trữ và định tuyến event đến đúng nơi. Đây là "bưu điện" của hệ thống. Các broker phổ biến gồm Apache Kafka, RabbitMQ, AWS SNS/SQS, Google Pub/Sub, Azure Event Hubs. Broker giúp producer và consumer không cần biết nhau — chúng chỉ cần biết broker.

Consumer (bên tiêu thụ) là dịch vụ đăng ký lắng nghe một loại event và phản ứng khi nhận được. Với OrderPlaced, ta có thể có nhiều consumer cùng lúc: dịch vụ Kho, dịch vụ Email, dịch vụ Điểm thưởng, dịch vụ Vận chuyển. Mỗi consumer xử lý độc lập, không biết về sự tồn tại của nhau.

Decoupling — giá trị lớn nhất

Lợi ích cốt lõi mà EDA mang lại là decoupling (tách rời sự phụ thuộc). Producer và consumer không gọi trực tiếp nhau, không cần biết địa chỉ của nhau, không cần online cùng lúc. Hệ quả thực tế rất mạnh:

  • Thêm tính năng không cần sửa code cũ. Muốn thêm dịch vụ "Phòng chống gian lận"? Chỉ cần viết một consumer mới lắng nghe OrderPlaced. Dịch vụ "Đặt hàng" hoàn toàn không cần đụng tới. Đây gọi là tuân thủ nguyên tắc Open/Closed ở cấp kiến trúc.
  • Chịu lỗi tốt hơn. Nếu dịch vụ Email tạm chết, đơn hàng vẫn được đặt, vẫn trừ kho. Khi Email sống lại, nó xử lý các event còn tồn trong broker. Lỗi không lan thành sụp đổ dây chuyền.
  • Mở rộng linh hoạt. Lúc cao điểm Flash Sale, bạn có thể tăng số instance của consumer Kho lên 10 lần mà không động chạm gì đến producer.

Hai mô hình truyền tin: Pub/Sub và Message Queue

Có hai cơ chế phổ biến cần phân biệt. Publish/Subscribe (Pub/Sub): một event được phát đi và mọi consumer đăng ký đều nhận được một bản sao. Phù hợp khi nhiều bên cùng quan tâm một sự kiện (Kho, Email, Điểm thưởng đều cần biết về OrderPlaced). Message Queue (hàng đợi): mỗi message chỉ được một consumer trong nhóm xử lý, dùng để chia tải công việc. Ví dụ, 5 worker cùng rút việc từ một hàng đợi "xử lý ảnh", mỗi ảnh chỉ một worker làm. Kafka và các hệ thống hiện đại thường kết hợp cả hai qua khái niệm "consumer group".

Event Notification vs Event-Carried State Transfer

Một sự phân biệt tinh tế nhưng quan trọng. Event notification chỉ báo "có chuyện xảy ra" kèm một ID, consumer muốn biết chi tiết phải gọi ngược lại hỏi producer. Event-carried state transfer thì nhét luôn dữ liệu cần thiết vào event, consumer không cần hỏi lại. Cách thứ hai giảm sự phụ thuộc nhưng làm event nặng hơn. Lựa chọn tùy bối cảnh, và đây là quyết định thiết kế bạn sẽ gặp thường xuyên.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT trong ngày Flash Sale 12.12

Một sàn TMĐT giả định tên MuaNhanh ban đầu xây hệ thống theo kiểu gọi trực tiếp: dịch vụ Đặt hàng gọi tuần tự 6 dịch vụ con. Vào ngày 12.12, lượng đơn tăng gấp 40 lần. Vấn đề lộ ra: dịch vụ Email của bên thứ ba bị nghẽn, mà vì gọi tuần tự, mỗi đơn hàng phải chờ email gửi xong mới hoàn tất. Kết quả: thời gian đặt hàng từ 0,8 giây vọt lên 12 giây, khách bỏ giỏ hàng hàng loạt, ước tính mất khoảng 3 tỷ đồng doanh thu trong 2 giờ.

Sau sự cố, đội kỹ thuật chuyển sang EDA với Apache Kafka. Giờ đây dịch vụ Đặt hàng chỉ phát một event OrderPlaced vào Kafka rồi trả kết quả cho khách ngay trong 0,3 giây. Email, điểm thưởng, vận chuyển trở thành các consumer xử lý bất đồng bộ phía sau. Khi Email lại bị nghẽn ở đợt 6.6 năm sau, event vẫn nằm yên trong Kafka chờ xử lý; khách hàng không hề bị ảnh hưởng, đơn hàng vẫn mượt mà.

Bài học: EDA biến các bước phụ thành bất đồng bộ, tách trải nghiệm khách hàng khỏi sự cố của các hệ thống phía sau. Đây là lý do gần như mọi sàn TMĐT lớn đều dựa trên EDA ở lõi.

Ví dụ 2 — Ngân hàng số và phát hiện gian lận thời gian thực

Một ngân hàng số tại Việt Nam cần phát hiện giao dịch bất thường. Trước đây, logic chống gian lận được nhúng thẳng vào luồng xử lý giao dịch, khiến mỗi lần cập nhật quy tắc phải deploy lại cả hệ thống core — rủi ro cực cao và phải xin cửa sổ bảo trì ban đêm.

Họ tái thiết kế: mỗi giao dịch phát ra event TransactionExecuted lên một broker. Một consumer riêng biệt — hệ thống fraud detection — lắng nghe luồng event này và chấm điểm rủi ro theo thời gian thực bằng mô hình machine learning. Khi phát hiện bất thường (ví dụ một thẻ quẹt ở Hà Nội rồi 3 phút sau quẹt ở TP.HCM), nó phát ra event FraudSuspected, kích hoạt một consumer khác tạm khóa thẻ và gửi cảnh báo qua app.

Cái hay là toàn bộ hệ thống chống gian lận tiến hóa độc lập với hệ thống giao dịch. Đội risk có thể nâng cấp mô hình ML hằng tuần mà không cần đụng tới core banking. Họ cũng "phát lại" (replay) toàn bộ lịch sử event để huấn luyện lại mô hình — một khả năng đặc trưng của các broker như Kafka vốn lưu giữ event như một dòng nhật ký bất biến.

Bài học: EDA cho phép xây dựng các năng lực phản ứng thời gian thực và cho phép các đội khác nhau tiến hóa độc lập — điều tối quan trọng trong môi trường ngân hàng vốn đề cao an toàn và kiểm soát.

Ví dụ 3 — Ứng dụng gọi xe và cập nhật vị trí

Một nền tảng gọi xe kiểu Grab/Be xử lý hàng triệu cập nhật vị trí tài xế mỗi phút. Mỗi khi tài xế di chuyển, app phát event DriverLocationUpdated. Nhiều consumer cùng quan tâm: dịch vụ ghép chuyến cần biết tài xế gần khách nhất, dịch vụ bản đồ cần vẽ vị trí xe, dịch vụ tính cước cần theo dõi quãng đường, dịch vụ phân tích cần dữ liệu để dự báo nhu cầu.

Nếu dùng kiểu gọi trực tiếp, mỗi cập nhật vị trí phải gọi 4 dịch vụ — bất khả thi ở quy mô hàng triệu sự kiện. Với EDA Pub/Sub, app tài xế chỉ phát một event, broker phân phối cho mọi consumer quan tâm. Thêm một dịch vụ mới (ví dụ "phát hiện tài xế đi sai tuyến") chỉ là thêm một subscriber.

Bài học: Ở quy mô lớn với dữ liệu luồng (streaming), EDA gần như là lựa chọn bắt buộc. Mô hình một-phát-nhiều-nhận của Pub/Sub là chìa khóa.

Hướng dẫn từng bước

Giả sử bạn muốn áp dụng EDA cho một tính năng cụ thể trong hệ thống của mình. Đây là lộ trình thực tế:

  • Xác định các "sự thật nghiệp vụ" đáng phát ra. Đừng phát mọi thứ. Hãy hỏi: sự kiện nào trong nghiệp vụ mà nhiều bên khác có thể quan tâm? OrderPlaced, PaymentConfirmed, UserRegistered là ứng viên tốt. Đặt tên theo thì quá khứ.
  • Thiết kế schema cho event. Quy định rõ cấu trúc: trường bắt buộc, kiểu dữ liệu, định danh. Dùng một định dạng chuẩn như JSON Schema hoặc Avro. Schema là "hợp đồng" giữa producer và consumer — cần ổn định và có phiên bản (versioning).
  • Chọn broker phù hợp. Cần lưu giữ và phát lại event, thông lượng cực cao? Chọn Apache Kafka. Cần định tuyến message linh hoạt, hàng đợi tác vụ truyền thống? RabbitMQ. Đang dùng cloud và muốn quản lý tối thiểu? AWS SNS/SQS, Google Pub/Sub hay Azure Event Hubs.
  • Viết producer. Trong code của dịch vụ, sau khi hoàn tất nghiệp vụ (ví dụ lưu đơn hàng vào database), phát event lên broker. Lưu ý mẫu "transactional outbox" (sẽ nói ở phần mẹo) để đảm bảo không mất event.
  • Viết consumer và đăng ký. Mỗi consumer đăng ký loại event nó quan tâm, xử lý và đảm bảo tính idempotent (xử lý lặp một event không gây hậu quả sai).
  • Thiết kế xử lý lỗi. Định nghĩa cơ chế retry và một Dead Letter Queue (DLQ) — nơi chứa các event xử lý thất bại nhiều lần để con người xem xét sau.
  • Giám sát. Theo dõi độ trễ, độ tồn đọng (consumer lag), tỷ lệ lỗi. Trong EDA, "luồng chảy" của event chính là mạch máu hệ thống — phải nhìn thấy được nó.

Lỗi thường gặp & mẹo

Lỗi 1 — Dùng event như command trá hình. Nhiều người đặt tên event kiểu SendEmail rồi chỉ một consumer duy nhất nghe. Đó thực chất là một command đội lốt event, làm mất ý nghĩa decoupling. Hãy giữ event ở dạng "thông báo sự thật" (OrderPlaced), để consumer tự quyết định làm gì.

Lỗi 2 — Bỏ quên tính idempotent. Broker thường đảm bảo "at-least-once delivery" — một event có thể được gửi lặp lại. Nếu consumer "cộng điểm thưởng" xử lý lặp, khách được cộng điểm hai lần. Mẹo: lưu event_id đã xử lý, gặp lại thì bỏ qua.

Lỗi 3 — Mất event do ghi DB và phát event không nguyên tử. Nếu bạn lưu đơn hàng vào DB thành công rồi mới phát event, nhưng tiến trình chết ngay giữa hai bước, event sẽ mất. Mẹo: dùng mẫu transactional outbox — ghi event vào một bảng trong cùng transaction với dữ liệu nghiệp vụ, rồi một tiến trình riêng đọc bảng đó và đẩy lên broker.

Lỗi 4 — "Địa ngục debug" vì luồng quá phân tán. Khi một đơn hàng đi qua 8 consumer, lỗi xảy ra ở đâu rất khó truy. Mẹo: gắn một correlation_id xuyên suốt mọi event của cùng một luồng, và dùng công cụ distributed tracing (như OpenTelemetry, Jaeger) để vẽ lại hành trình.

Lỗi 5 — Thay đổi schema phá vỡ consumer. Producer thêm/đổi trường, consumer cũ vỡ. Mẹo: chỉ thêm trường mới (backward compatible), không xóa/đổi tên trường cũ; dùng schema registry để quản lý phiên bản.

Mẹo lớn: Đừng "EDA hóa" mọi thứ. EDA tăng độ phức tạp vận hành (cần broker, cần giám sát, cần xử lý lỗi phân tán). Với hệ thống nhỏ hoặc luồng đồng bộ đơn giản, gọi API trực tiếp vẫn tốt hơn. Hãy dùng EDA ở nơi có nhiều bên quan tâm cùng một sự kiện, hoặc nơi cần bất đồng bộ và mở rộng quy mô.

Bài tập thực hành

  • Phân tích nghiệp vụ. Chọn một quy trình quen thuộc (đặt vé xem phim, đăng ký khóa học online, hoàn tất một đơn hàng GrabFood). Liệt kê ít nhất 4 event đáng phát ra, đặt tên đúng quy ước thì quá khứ.
  • Vẽ sơ đồ Producer–Broker–Consumer. Với event OrderPlaced của một sàn TMĐT, vẽ ra: ai là producer, broker nào, và ít nhất 3 consumer cùng vai trò của mỗi consumer.
  • Thiết kế schema. Viết một event PaymentConfirmed dạng JSON với đầy đủ: event_type, event_id, occurred_at, correlation_id và phần data hợp lý cho bối cảnh thanh toán tại Việt Nam (VND).
  • Tình huống xử lý lỗi. Consumer "gửi SMS xác nhận" nhận cùng một event hai lần do broker gửi lặp. Mô tả bằng lời cách bạn làm cho consumer này idempotent.
  • Ra quyết định kiến trúc. Cho một startup có 3 dịch vụ nhỏ và lưu lượng thấp, hãy lập luận: nên dùng EDA hay gọi API trực tiếp? Nêu rõ căn cứ.

Tóm tắt

Event-Driven Architecture xoay quanh một ý tưởng đơn giản nhưng mạnh mẽ: các dịch vụ giao tiếp bằng cách thông báo sự kiện đã xảy ra, thay vì gọi trực tiếp ra lệnh cho nhau. Bộ ba Producer — Broker — Consumer là xương sống: producer phát event, broker lưu trữ và định tuyến, consumer lắng nghe và phản ứng độc lập.

Giá trị cốt lõi là decoupling: producer và consumer không cần biết nhau, không cần online cùng lúc. Nhờ đó hệ thống dễ mở rộng, chịu lỗi tốt, và cho phép thêm tính năng mới mà không phải sửa code cũ. Bạn cần nắm hai mô hình truyền tin (Pub/Sub và Message Queue), phân biệt event với command, và làm chủ các thực hành quan trọng: idempotency, transactional outbox, correlation ID, dead letter queue và quản lý schema.

Cuối cùng, hãy nhớ rằng EDA không phải "viên đạn bạc". Nó đánh đổi độ phức tạp vận hành để lấy sự linh hoạt và khả năng mở rộng. Hãy dùng nó đúng chỗ — nơi nhiều bên cùng quan tâm một sự kiện, nơi cần xử lý bất đồng bộ và quy mô lớn. Khi áp dụng đúng, EDA chính là nền móng cho những hệ thống automation và chuyển đổi số bền vững nhất.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi