Software Architecture

Event-Driven Architecture (EDA) từ A đến Z

Admin · Cập nhật 19/07/2026

Event-Driven Architecture (EDA) là gì: Producer/Broker/Consumer, luồng hoạt động, so sánh với Request/Response, ưu nhược điểm và best practices triển khai.

Chuyên mục: Software Architecture

Mục lục

  1. Event-Driven Architecture là gì?
  2. Thành phần chính
  3. Event Producer, Broker, Consumer
  4. Luồng hoạt động
  5. Ví dụ thực tế
  6. So sánh với Request/Response
  7. Ưu điểm
  8. Nhược điểm
  9. Best Practices
  10. Anti-Patterns
  11. Checklist
  12. FAQ

Event-Driven Architecture là gì?

Event-Driven Architecture (EDA) là mô hình kiến trúc trong đó các thành phần của hệ thống giao tiếp với nhau thông qua Event (Sự kiện) thay vì gọi trực tiếp lẫn nhau.

Ví dụ:

  • Đơn hàng được tạo.
  • Người dùng đăng ký tài khoản.
  • Thanh toán thành công.
  • Email được gửi.

Mỗi hành động sẽ phát sinh một Event để các dịch vụ khác xử lý.


Thành phần chính

Event

Một thông báo cho biết điều gì đã xảy ra.

{
  "event": "OrderCreated",
  "orderId": "12345",
  "createdAt": "2026-07-19T10:00:00Z"
}

Event Producer

Thành phần phát sinh sự kiện.

Ví dụ:

  • Order Service
  • Payment Service

Event Broker

Hệ thống trung gian truyền sự kiện.

Ví dụ:

  • Kafka
  • RabbitMQ
  • Azure Service Bus
  • Google Pub/Sub

Event Consumer

Thành phần lắng nghe và xử lý sự kiện.

Ví dụ:

  • Email Service
  • Notification Service
  • Analytics Service

Luồng hoạt động

Order Service
      │
      ▼
Publish OrderCreated
      │
      ▼
 Message Broker
 ├── Email Service
 ├── Inventory Service
 ├── Notification Service
 └── Analytics Service

Ví dụ

Người dùng đặt hàng:

  1. Order Service tạo đơn hàng.
  2. Phát sự kiện OrderCreated.
  3. Inventory Service trừ tồn kho.
  4. Email Service gửi email xác nhận.
  5. Analytics Service ghi nhận doanh thu.

Các dịch vụ không cần gọi trực tiếp nhau.


Ví dụ code: Producer và Consumer

Dưới đây là minh họa tối giản bằng TypeScript. Producer định nghĩa Event dưới dạng kiểu bất biến rồi phát nó qua Broker:

// Định nghĩa Event như một hợp đồng (contract) bất biến
export interface OrderCreatedEvent {
  type: "OrderCreated";
  orderId: string;
  userId: string;
  total: number;
  occurredAt: string;
}

// Producer: sau khi tạo đơn hàng thì phát sự kiện
export class OrderService {
  constructor(private broker: MessageBroker) {}

  async createOrder(input: CreateOrderInput): Promise<void> {
    const order = await saveOrderToDb(input);

    const event: OrderCreatedEvent = {
      type: "OrderCreated",
      orderId: order.id,
      userId: order.userId,
      total: order.total,
      occurredAt: new Date().toISOString(),
    };

    await this.broker.publish("orders", event);
  }
}

Consumer lắng nghe topic và xử lý. Chú ý cơ chế idempotency: mỗi Event có thể được giao nhiều lần (at-least-once), nên Consumer phải bỏ qua Event đã xử lý để tránh gửi email hai lần:

export class SendEmailConsumer {
  constructor(private processed: ProcessedEventStore) {}

  async handle(event: OrderCreatedEvent): Promise<void> {
    // Idempotency: nếu đã xử lý event này rồi thì bỏ qua
    if (await this.processed.has(event.orderId)) {
      return;
    }

    await sendConfirmationEmail(event.userId, event.orderId);
    await this.processed.markDone(event.orderId);
  }
}

Ví dụ thực tế với Next.js + Prisma (Outbox Pattern)

Một thách thức kinh điển của EDA là: làm sao đảm bảo lưu dữ liệu vào DBphát Event xảy ra nguyên tử? Nếu bạn lưu đơn hàng thành công rồi mới gọi broker mà broker lỗi, Event sẽ bị mất. Outbox Pattern giải quyết điều này: ghi Event vào một bảng outbox trong cùng transaction với dữ liệu nghiệp vụ.

Trước tiên, thêm bảng outbox vào schema.prisma:

model OutboxEvent {
  id        String   @id @default(cuid())
  type      String
  payload   Json
  published Boolean  @default(false)
  createdAt DateTime @default(now())
}

Khi tạo đơn hàng, ghi cả Order lẫn OutboxEvent trong một $transaction. Vì cùng transaction, hai thao tác này cùng thành công hoặc cùng thất bại:

// app/api/orders/route.ts
import { prisma } from "@/lib/prisma";

async function createOrder(input: CreateOrderInput) {
  return prisma.$transaction(async (tx) => {
    const order = await tx.order.create({ data: input });

    await tx.outboxEvent.create({
      data: {
        type: "OrderCreated",
        payload: { orderId: order.id, userId: order.userId, total: order.total },
      },
    });

    return order;
  });
}

Một tiến trình nền (background worker, hoặc một Route Handler cron) sẽ đọc các Event chưa gửi và đẩy lên broker, sau đó đánh dấu đã publish:

async function dispatchOutbox() {
  const events = await prisma.outboxEvent.findMany({
    where: { published: false },
    take: 50,
    orderBy: { createdAt: "asc" },
  });

  for (const event of events) {
    await broker.publish("orders", { type: event.type, ...(event.payload as object) });
    await prisma.outboxEvent.update({
      where: { id: event.id },
      data: { published: true },
    });
  }
}

Nhờ Outbox Pattern, hệ thống không bao giờ rơi vào tình trạng "đơn hàng đã tạo nhưng Event bị mất", đồng thời vẫn giữ được tính bất đồng bộ của EDA.


So sánh với Request/Response

Tiêu chí Request/Response Event-Driven
Coupling Cao Thấp
Đồng bộ Thường bất đồng bộ
Khả năng mở rộng Trung bình Cao
Thêm chức năng mới Phải sửa service Chỉ thêm consumer

Ưu điểm

  • Giảm phụ thuộc giữa các dịch vụ.
  • Dễ mở rộng.
  • Hỗ trợ xử lý bất đồng bộ.
  • Phù hợp Microservices.

Nhược điểm

  • Khó debug.
  • Khó theo dõi luồng xử lý.
  • Cần cơ chế retry, idempotency và monitoring.

Khi nào nên và không nên dùng EDA

Tình huống Nên dùng EDA? Lý do
Nhiều dịch vụ cần phản ứng với cùng một hành động Chỉ cần thêm Consumer, không sửa Producer
Tác vụ nặng, chạy nền (gửi email, xuất báo cáo) Xử lý bất đồng bộ, không chặn người dùng
Hệ thống Microservices cần giảm coupling Các dịch vụ giao tiếp qua Event, ít phụ thuộc
Cần phản hồi tức thì cho người dùng Không Request/Response đơn giản và trực tiếp hơn
Ứng dụng CRUD nhỏ, một service Không EDA thêm hạ tầng và độ phức tạp không đáng
Quy trình cần tính nhất quán mạnh, tức thời Cân nhắc EDA thường là eventual consistency

Best Practices

  • Event chỉ mô tả việc đã xảy ra.
  • Thiết kế Event bất biến.
  • Mỗi Consumer xử lý một nhiệm vụ.
  • Dùng Dead Letter Queue cho lỗi.
  • Thiết kế Consumer idempotent.

Anti-Patterns

  • Đưa business logic vào Broker.
  • Event quá lớn chứa dữ liệu không cần thiết.
  • Consumer phụ thuộc chặt vào Producer.
  • Không xử lý retry.

Sai lầm thường gặp

  • Consumer không idempotent. Hầu hết broker giao Event theo cơ chế at-least-once, nghĩa là một Event có thể đến nhiều lần. Nếu Consumer không kiểm tra đã xử lý hay chưa, người dùng có thể bị trừ tiền hai lần hoặc nhận email trùng. Luôn thiết kế Consumer để xử lý lặp lại một cách an toàn.
  • Ghi DB và phát Event ở hai bước tách rời. Lưu đơn hàng thành công rồi mới gọi broker, nếu broker lỗi thì Event bị mất và các dịch vụ khác không bao giờ biết đơn hàng tồn tại. Hãy dùng Outbox Pattern để hai việc này nằm trong cùng một transaction.
  • Nhét quá nhiều dữ liệu vào Event. Event khổng lồ chứa cả object lồng nhiều tầng khiến hợp đồng khó tiến hóa và tăng tải cho broker. Chỉ đưa vào Event những thông tin tối thiểu; nếu Consumer cần thêm, nó có thể truy vấn ngược.
  • Bỏ qua Event thất bại. Không có Dead Letter Queue (DLQ) đồng nghĩa với việc một Event lỗi sẽ biến mất lặng lẽ hoặc chặn cả hàng đợi. Hãy định tuyến Event xử lý thất bại sau nhiều lần retry vào DLQ để điều tra sau.
  • Coupling ngầm qua thứ tự Event. Consumer giả định các Event luôn đến đúng thứ tự (ví dụ OrderPaid luôn sau OrderCreated) sẽ vỡ khi broker giao không theo thứ tự. Hãy thiết kế Consumer chịu được Event đến lệch thứ tự, hoặc dùng khóa phân vùng (partition key) khi thật cần thứ tự.
  • Thiếu logging và tracing. Vì luồng xử lý bất đồng bộ và phân tán, không có correlation ID hay tracing thì việc debug gần như bất khả thi. Gắn một ID xuyên suốt để lần theo một Event qua tất cả các dịch vụ.

Checklist

  • Event có tên rõ ràng.
  • Có cơ chế retry.
  • Có Dead Letter Queue.
  • Consumer idempotent.
  • Có logging và tracing.

FAQ

EDA có bắt buộc dùng Kafka?

Không. Có thể dùng RabbitMQ, Azure Service Bus, Google Pub/Sub hoặc các hệ thống hàng đợi khác.

Có nên dùng EDA cho mọi dự án?

Không. Với ứng dụng CRUD nhỏ, Request/Response đơn giản hơn. EDA phát huy hiệu quả khi hệ thống lớn, nhiều dịch vụ hoặc cần xử lý bất đồng bộ.

Idempotency trong EDA nghĩa là gì?

Idempotency là khả năng xử lý cùng một Event nhiều lần mà kết quả không thay đổi. Vì broker thường giao Event ít nhất một lần (at-least-once), Consumer phải lưu lại các Event đã xử lý (theo một ID duy nhất) và bỏ qua nếu gặp lại. Đây là điều kiện gần như bắt buộc để EDA hoạt động đúng đắn.

Outbox Pattern giải quyết vấn đề gì?

Nó giải quyết bài toán "dual write": khi bạn phải vừa ghi DB vừa phát Event nhưng không thể làm cả hai một cách nguyên tử. Outbox cho phép 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 nền mới đẩy Event lên broker. Nhờ vậy Event không bao giờ bị mất dù broker tạm thời lỗi.

Sự khác nhau giữa Event và Command là gì?

Command là một yêu cầu thực hiện hành động và thường gửi tới đúng một người nhận (ví dụ CreateOrder). Event là một thông báo rằng điều gì đó đã xảy ra (ví dụ OrderCreated) và có thể được nhiều Consumer quan tâm. Event mô tả quá khứ, bất biến; Command hướng tới tương lai và có thể bị từ chối.

EDA có đảm bảo dữ liệu luôn nhất quán tức thời không?

Không. EDA thiên về eventual consistency: sau khi Producer phát Event, các Consumer cập nhật trạng thái của mình sau một khoảng trễ. Nếu nghiệp vụ đòi hỏi tính nhất quán mạnh và tức thời (như trừ tiền trong cùng một giao dịch), bạn nên dùng transaction trực tiếp thay vì Event.


Kết luận

Event-Driven Architecture là nền tảng quan trọng của các hệ thống hiện đại. Khi kết hợp với Microservices, CQRS và Outbox Pattern, EDA giúp hệ thống linh hoạt, dễ mở rộng và có khả năng xử lý tải cao.