Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang xây dựng một sàn thương mại điện tử như Tiki hay Shopee. Khi một khách hàng bấm nút "Đặt hàng", có bao nhiêu việc cần xảy ra? Trừ tiền trong ví, trừ hàng trong kho, gửi email xác nhận, tạo đơn cho bên giao vận, cộng điểm thưởng, cập nhật báo cáo doanh thu, và có thể gửi cả thông báo đẩy về điện thoại. Nếu service "Đặt hàng" phải tự tay gọi API tới từng service kia rồi chờ tất cả trả lời xong mới báo "đặt hàng thành công", thì chỉ cần một service (ví dụ service gửi email) bị chậm hay sập, toàn bộ luồng đặt hàng sẽ đứng lại. Khách hàng nhìn màn hình quay quay 30 giây rồi bỏ đ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. Thay vì các service ra lệnh trực tiếp cho nhau ("mày hãy làm việc này ngay"), chúng chỉ đơn giản thông báo "một chuyện đã xảy ra" (an event happened) và mặc kệ ai quan tâm thì tự xử lý. Đây là một trong những mẫu kiến trúc nền tảng của các hệ thống cloud hiện đại, quy mô lớn, và là kiến thức bắt buộc nếu bạn muốn thiết kế hệ thống chịu tải cao, dễ mở rộng, và bền bỉ trước sự cố. Ở các bài trước bạn đã học về Message Queue (Bài 24), Streaming (Bài 25) và các mẫu Serverless Architecture (Bài 45) — bài này sẽ ghép chúng lại thành một triết lý thiết kế hoàn chỉnh.
Khái niệm cốt lõi
Command vs Event — hai cách giao tiếp trái ngược
Điểm mấu chốt của EDA nằm ở sự khác biệt giữa hai kiểu giao tiếp:
- Command (mệnh lệnh): "Hãy làm việc gì đó" (do something). Ví dụ:
SendConfirmationEmail. Người gửi biết chính xác ai sẽ nhận, mong đợi việc đó được thực hiện, và thường chờ kết quả. Đây là kiểu gọi API trực tiếp (request/response) mà bạn quen thuộc.
- Event (sự kiện): "Một chuyện đã xảy ra rồi" (something happened). Ví dụ:
OrderPlaced(đơn hàng đã được đặt). Người phát ra event không quan tâm ai sẽ nghe, cũng không mong đợi câu trả lời. Event là một sự thật trong quá khứ, bất biến — đơn hàng đã được đặt là đã được đặt, không thể "hủy" cái sự thật đó.
OrderPlaced, mà không phải sửa một dòng code nào trong service đặt hàng.Ba thành phần chính
Một hệ thống EDA thường có ba vai trò:
- Producer (Event Producer / Publisher): service phát ra sự kiện khi có gì đó xảy ra. Ví dụ Order Service phát ra
OrderPlaced.
- Event Broker (Event Router / Bus): hạ tầng trung gian nhận event và chuyển đến những ai đăng ký. Trên cloud, đây là các dịch vụ như AWS EventBridge, SNS, Kafka, Google Pub/Sub, Azure Event Grid.
- Consumer (Event Consumer / Subscriber): service lắng nghe và phản ứng với event. Ví dụ Email Service, Inventory Service, Loyalty Service đều nghe
OrderPlacedvà mỗi anh làm phần việc của mình.
Hai topology phổ biến
Pub/Sub (Publish-Subscribe): Producer phát event lên một "kênh" (topic), tất cả các consumer đã đăng ký kênh đó đều nhận được một bản sao. Kiểu này giống như phát thanh — một đài phát, nhiều người nghe. Phù hợp khi nhiều bên độc lập cùng quan tâm một sự kiện.
Event Streaming: Event được ghi vào một "sổ cái" (log) bất biến, có thứ tự, được lưu giữ lâu dài (ví dụ Kafka, Kinesis). Consumer có thể "tua lại" đọc từ đầu, và các consumer khác nhau đọc ở tốc độ khác nhau. Kiểu này phù hợp cho phân tích dữ liệu, tính toán real-time, và khi bạn cần lịch sử đầy đủ của các sự kiện.
Choreography vs Orchestration
Khi nhiều service phối hợp qua event, có hai cách tổ chức:
- Choreography (biên đạo): mỗi service tự phản ứng với event và tự phát ra event tiếp theo, không có "nhạc trưởng". Như vũ đoàn mà mỗi vũ công tự biết khi nào vào theo nhạc. Linh hoạt, ghép lỏng, nhưng khó theo dõi toàn bộ luồng.
- Orchestration (điều phối): có một service trung tâm (orchestrator) điều khiển toàn bộ quy trình, ra lệnh cho từng service theo trình tự. Dễ nhìn thấy toàn cảnh và xử lý lỗi, nhưng orchestrator trở thành điểm ghép chặt. Trên AWS, dịch vụ Step Functions thường đóng vai orchestrator.
Tình huống thực tế
Ví dụ 1: Sàn TMĐT chuyển từ monolith gọi API sang EDA
Một công ty TMĐT Việt Nam (giả định tên ChoTot Express, xử lý khoảng 50.000 đơn/ngày) ban đầu có một Order Service gọi tuần tự 6 service khác qua REST API. Vấn đề: mỗi khi có chương trình flash sale, lượng đơn tăng gấp 10 lần, service gửi email (dùng nhà cung cấp bên thứ ba) bị nghẽn, kéo theo toàn bộ luồng đặt hàng chậm lại. Tỷ lệ đặt hàng thất bại lên tới 8% trong giờ cao điểm — tương đương hàng nghìn đơn mất mỗi ngày sale.
Đội kỹ thuật tái thiết kế theo EDA: Order Service chỉ làm hai việc — ghi đơn vào database và phát event OrderPlaced lên AWS EventBridge, rồi trả về "thành công" cho khách trong vòng 200ms. Sáu service kia trở thành consumer, mỗi anh tự xử lý bất đồng bộ với tốc độ riêng. Kết quả: khi service email chậm, các event chỉ nằm chờ trong hàng đợi và được xử lý dần khi service khỏe lại, không hề ảnh hưởng đến việc khách đặt hàng. Tỷ lệ thất bại giảm từ 8% xuống dưới 0,3%.
Bài học rút ra: EDA giúp tách phần "phải nhanh và luôn sẵn sàng" (nhận đơn) khỏi phần "có thể chậm một chút cũng được" (gửi email, cập nhật báo cáo). Đây gọi là hấp thụ tải (load buffering) — hàng đợi event trở thành tấm đệm chống sốc.
Ví dụ 2: Grab và luồng cập nhật vị trí tài xế
Một nền tảng gọi xe kiểu Grab phát ra hàng triệu event DriverLocationUpdated mỗi phút. Nếu dùng kiểu gọi API trực tiếp, không hệ thống nào chịu nổi. Thay vào đó họ dùng event streaming (Kafka/Kinesis): mỗi cập nhật vị trí là một event ghi vào stream. Nhiều consumer khác nhau cùng đọc stream này với mục đích khác nhau — một anh cập nhật bản đồ real-time cho khách, một anh tính toán ETA (thời gian đến dự kiến), một anh phân tích để tối ưu điều phối xe, một anh lưu vào data warehouse (Bài 17) để phân tích sau.
Điểm hay: nếu ngày mai đội Data Science muốn xây một mô hình dự đoán nhu cầu theo khu vực, họ chỉ cần tạo một consumer mới đọc stream DriverLocationUpdated từ quá khứ, không cần ai phải thay đổi hệ thống đang chạy.
Bài học rút ra: Với dữ liệu tần suất cao và nhiều bên tiêu thụ độc lập, event streaming cho phép "một nguồn, nhiều cách dùng" và khả năng tua lại lịch sử — điều mà gọi API trực tiếp không thể làm được.
Ví dụ 3: Cái bẫy khi làm EDA sai
Một startup fintech (giả định VíXanh) hào hứng chuyển toàn bộ sang EDA, kể cả luồng chuyển tiền. Khi người dùng chuyển tiền, họ phát event TransferRequested và để các service xử lý bất đồng bộ. Vấn đề nảy sinh: người dùng bấm chuyển 2 lần vì mạng chậm, hệ thống nhận 2 event, trừ tiền 2 lần. Tệ hơn, do không có thứ tự đảm bảo, có lúc event AccountDebited (đã trừ tiền) được xử lý trước khi kiểm tra số dư hoàn tất.
Họ phải khắc phục bằng idempotency (mỗi event có ID duy nhất, xử lý 2 lần cũng chỉ tính 1) và chuyển phần nhạy cảm sang orchestration với Step Functions để đảm bảo trình tự.
Bài học rút ra: EDA không phải viên đạn bạc. Với các luồng cần tính nhất quán mạnh và trình tự nghiêm ngặt (đặc biệt là tiền bạc), bạn phải thiết kế idempotency và cân nhắc orchestration thay vì choreography thuần túy.
Hướng dẫn từng bước
Giả sử bạn muốn áp dụng EDA cho luồng đặt hàng. Đây là các bước thực tế:
- Xác định các sự kiện nghiệp vụ (domain events). Ngồi cùng đội nghiệp vụ, liệt kê những "chuyện đã xảy ra" đáng quan tâm:
OrderPlaced,PaymentCompleted,OrderShipped,OrderCancelled. Đặt tên ở thì quá khứ để nhắc bản thân rằng đây là sự thật đã rồi.
- Thiết kế nội dung (schema) của event. Mỗi event nên chứa đủ thông tin để consumer xử lý mà không phải gọi ngược lại hỏi thêm:
eventId,eventType,timestamp, và phần payload (mã đơn, mã khách, tổng tiền...). Đặt phiên bản (version) cho schema ngay từ đầu để sau này dễ nâng cấp.
- Chọn event broker phù hợp. Cần fan-out đơn giản, nhiều consumer độc lập? Dùng SNS hoặc EventBridge. Cần định tuyến theo quy tắc phức tạp? EventBridge. Cần lưu lịch sử, tua lại, thông lượng cực cao? Kafka hoặc Kinesis. Trên GCP là Pub/Sub, trên Azure là Event Grid / Event Hubs.
- Viết producer. Trong Order Service, sau khi ghi đơn vào DB thành công, phát event. Lưu ý điểm quan trọng ở bước 6 về tính nhất quán.
- Viết các consumer. Mỗi consumer đăng ký loại event nó quan tâm và xử lý độc lập. Consumer phải idempotent — nhận cùng một event 2 lần thì kết quả vẫn như nhận 1 lần.
- Xử lý bài toán "dual write". Đây là cạm bẫy kinh điển: nếu bạn ghi DB rồi phát event ở hai bước riêng, có thể ghi DB xong thì service crash trước khi kịp phát event — dữ liệu và event lệch nhau. Giải pháp phổ biến là Transactional Outbox: ghi event vào một bảng "outbox" trong cùng transaction với đơn hàng, rồi một tiến trình riêng đọc bảng đó và phát event ra broker.
- Thiết lập Dead Letter Queue (DLQ). Event nào xử lý thất bại nhiều lần sẽ được đẩy vào DLQ để không chặn hàng đợi và để bạn điều tra sau.
- Giám sát và truy vết. Vì luồng chạy bất đồng bộ qua nhiều service, bạn cần distributed tracing (Bài 35) để lần theo một đơn hàng đi qua những consumer nào, và cần giám sát độ trễ hàng đợi, số lượng message tồn đọng.
Lỗi thường gặp & mẹo
- Biến event thành command trá hình. Đặt tên event kiểu
SendEmailCommandrồi gửi qua broker — về hình thức là event nhưng bản chất vẫn ghép chặt vì producer đã "ra lệnh". Hãy giữ event ở dạng thông báo trung lập:OrderPlaced, để consumer tự quyết định làm gì.
- Quên idempotency. Đây là lỗi nguy hiểm nhất. Hầu hết các broker đảm bảo "at-least-once delivery" (giao ít nhất một lần) — nghĩa là một event có thể được giao 2, 3 lần. Nếu consumer không idempotent, bạn sẽ trừ tiền 2 lần, gửi email 2 lần. Mẹo: lưu lại
eventIdđã xử lý, gặp lại thì bỏ qua.
- Payload quá mỏng hoặc quá dày. Quá mỏng (chỉ có ID) khiến consumer phải gọi ngược lại hỏi thông tin — làm hỏng tính ghép lỏng. Quá dày (nhét cả object khổng lồ) khiến event nặng và khó tiến hóa. Hãy chứa vừa đủ thông tin cho các consumer chính.
- Không quản lý phiên bản schema. Một ngày bạn cần thêm trường mới vào event, mà consumer cũ chưa cập nhật sẽ vỡ. Mẹo: chỉ thêm trường (backward compatible), không xóa/đổi tên trường cũ; đánh version rõ ràng.
- Lạm dụng EDA cho mọi thứ. Với luồng cần trả lời ngay lập tức cho người dùng (ví dụ đăng nhập, kiểm tra số dư), gọi API đồng bộ vẫn đơn giản và đúng hơn. EDA tỏa sáng khi công việc có thể xử lý bất đồng bộ và có nhiều bên quan tâm.
- Bỏ qua khả năng quan sát. Trong EDA, khi có lỗi bạn không thể đọc một stack trace duy nhất. Đầu tư vào tracing, correlation ID (mã liên kết đi xuyên các event) và dashboard giám sát hàng đợi ngay từ đầu, đừng để đến khi sự cố mới hối hận.
Bài tập thực hành
- Phân tích luồng: Lấy luồng "đăng ký tài khoản mới" trên một ứng dụng bạn dùng (ví dụ gửi email chào mừng, tạo hồ sơ mặc định, cộng khuyến mãi người mới, thông báo cho đội sales). Hãy vẽ ra: đâu là event
UserRegistered, ai là các consumer, và giải thích vì sao dùng EDA sẽ tốt hơn gọi API tuần tự.
- Thiết kế schema: Viết ra cấu trúc JSON cho event
OrderPlacedvới đầy đủeventId,eventType,timestamp,versionvà payload. Sau đó liệt kê 3 consumer khác nhau và mỗi anh sẽ dùng những trường nào.
- Xử lý tình huống: Consumer gửi email của bạn thỉnh thoảng nhận cùng một event
OrderPlacedhai lần, khiến khách nhận 2 email trùng. Hãy mô tả cách bạn làm cho consumer này idempotent (gợi ý: dùng bảng lưueventIdđã xử lý).
- So sánh: Cho tình huống chuyển tiền trong app fintech, hãy lập luận nên dùng choreography hay orchestration, và giải thích tại sao. Nêu tối thiểu 2 lý do.
Tóm tắt
Event-driven Architecture là kiến trúc trong đó các service giao tiếp bằng cách phát ra sự kiện ("một chuyện đã xảy ra") thay vì mệnh lệnh ("hãy làm việc này"). Sự thay đổi tư duy này mang lại ba lợi ích lớn: ghép lỏng (thêm tính năng mới không cần sửa code cũ), khả năng mở rộng và bền bỉ (hàng đợi hấp thụ tải, một service sập không kéo cả hệ thống), và linh hoạt trong xử lý dữ liệu (một nguồn event, nhiều cách tiêu thụ độc lập).
Ba thành phần cốt lõi là Producer, Event Broker và Consumer, với hai topology chính là Pub/Sub và Event Streaming, cùng hai cách phối hợp là Choreography và Orchestration. Trên cloud, bạn hiện thực EDA bằng các dịch vụ như EventBridge, SNS, Kafka, Kinesis, Pub/Sub hay Event Grid.
Nhưng hãy nhớ: EDA không miễn phí. Bạn phải trả giá bằng việc thiết kế idempotency, xử lý bài toán dual write (Transactional Outbox), quản lý schema versioning, và đầu tư mạnh vào giám sát và tracing. Dùng đúng chỗ — cho công việc bất đồng bộ có nhiều bên quan tâm — EDA là nền tảng của những hệ thống cloud quy mô lớn nhất thế giới. Dùng sai chỗ — cho luồng cần trả lời tức thì và nhất quán mạnh — nó sẽ mang lại rắc rối nhiều hơn lợi ích.