system-design

Event-Driven Architecture: Khi nào nên dùng Events thay vì API calls

Event-Driven Architecture không phải cho mọi hệ thống. Đây là decision framework và patterns thực tế từ kinh nghiệm build distributed systems.

newspaper

Phạm Hoàng Long

5 tháng 1, 2026 schedule 8 phút đọc
Event-Driven Architecture: Khi nào nên dùng Events thay vì API calls
Featured Image

Năm 2022, tôi build một e-commerce platform với 12 microservices.

Approach đầu tiên: Mọi service gọi API của nhau.

Kết quả: Sau 3 tháng, dependency hell. Order Service phải gọi:

  • Inventory Service (check stock)
  • Payment Service (process payment)
  • Shipping Service (create shipment)
  • Email Service (send confirmation)
  • Analytics Service (track order)

Một service down → Toàn bộ flow fail.

Approach thứ hai: Event-Driven Architecture.

Order Service chỉ publish event: OrderPlaced. Các services khác subscribe và xử lý độc lập.

Kết quả: Decoupled, resilient, scalable.

Đây là những gì tôi học được.

Event-Driven vs Request-Response

Request-Response (Traditional)

Order Service
    ↓ (HTTP call)
Inventory Service (check stock)
    ↓ (HTTP call)
Payment Service (charge card)
    ↓ (HTTP call)
Email Service (send confirmation)

Characteristics:

  • Synchronous
  • Tightly coupled
  • Caller phải đợi response
  • Failure ở bất kỳ step nào → Toàn bộ flow fail

Code:

// order-service/controllers/order.js
async function createOrder(req, res) {
  try {
    // Check inventory
    const inventory = await fetch('http://inventory-service/check', {
      method: 'POST',
      body: JSON.stringify({ productId: req.body.productId })
    }).then(r => r.json());
    
    if (!inventory.available) {
      return res.status(400).json({ error: 'Out of stock' });
    }
    
    // Process payment
    const payment = await fetch('http://payment-service/charge', {
      method: 'POST',
      body: JSON.stringify({ amount: req.body.amount })
    }).then(r => r.json());
    
    if (!payment.success) {
      return res.status(400).json({ error: 'Payment failed' });
    }
    
    // Create order
    const order = await Order.create(req.body);
    
    // Send email
    await fetch('http://email-service/send', {
      method: 'POST',
      body: JSON.stringify({ orderId: order.id })
    });
    
    res.json(order);
  } catch (error) {
    // Rollback? Compensating transactions?
    res.status(500).json({ error: 'Order creation failed' });
  }
}

Vấn đề:

  • Nếu Email Service down → Order creation fail (dù payment đã success)
  • Tight coupling
  • Hard to scale

Event-Driven

Order Service
    ↓ (publish event)
Message Broker (Kafka/RabbitMQ)
    ↓ ↓ ↓ ↓
    ↓ ↓ ↓ └→ Analytics Service
    ↓ ↓ └→ Email Service
    ↓ └→ Shipping Service
    └→ Inventory Service

Characteristics:

  • Asynchronous
  • Loosely coupled
  • Publisher không biết consumers
  • Failure ở một consumer không ảnh hưởng khác

Code:

// order-service/controllers/order.js
async function createOrder(req, res) {
  // Create order
  const order = await Order.create(req.body);
  
  // Publish event
  await eventBus.publish('OrderPlaced', {
    orderId: order.id,
    userId: order.userId,
    items: order.items,
    total: order.total,
    timestamp: new Date()
  });
  
  // Return immediately
  res.json(order);
}

// inventory-service/consumers/order-placed.js
eventBus.subscribe('OrderPlaced', async (event) => {
  await Inventory.decreaseStock(event.items);
});

// email-service/consumers/order-placed.js
eventBus.subscribe('OrderPlaced', async (event) => {
  await sendEmail(event.userId, 'Order Confirmation', event);
});

// shipping-service/consumers/order-placed.js
eventBus.subscribe('OrderPlaced', async (event) => {
  await Shipment.create({ orderId: event.orderId });
});

Pros:

  • Decoupled
  • Resilient (Email Service down không ảnh hưởng order creation)
  • Easy to add new consumers

Cons:

  • Eventual consistency
  • Debugging khó hơn
  • Complexity tăng

Khi nào NÊN dùng Events?

Use Case 1: Fan-out (1 event → nhiều actions)

Scenario: User đăng ký account

Actions cần thực hiện:

  • Tạo user record
  • Gửi welcome email
  • Tạo default settings
  • Track analytics
  • Notify admin

Event-Driven:

// user-service
await eventBus.publish('UserRegistered', {
  userId: user.id,
  email: user.email,
  timestamp: new Date()
});

// email-service
eventBus.subscribe('UserRegistered', async (event) => {
  await sendWelcomeEmail(event.email);
});

// settings-service
eventBus.subscribe('UserRegistered', async (event) => {
  await Settings.create({ userId: event.userId, defaults: {...} });
});

// analytics-service
eventBus.subscribe('UserRegistered', async (event) => {
  await Analytics.track('user_registered', event);
});

Benefit: Dễ dàng thêm actions mới mà không modify User Service.

Use Case 2: Long-running processes

Scenario: Video processing

Flow:

  1. User upload video
  2. Transcode video (mất 5-10 phút)
  3. Generate thumbnail
  4. Extract metadata
  5. Notify user

Event-Driven:

// upload-service
await eventBus.publish('VideoUploaded', {
  videoId: video.id,
  url: video.url
});

// transcode-service
eventBus.subscribe('VideoUploaded', async (event) => {
  const transcoded = await transcodeVideo(event.url);
  await eventBus.publish('VideoTranscoded', {
    videoId: event.videoId,
    transcodedUrl: transcoded.url
  });
});

// thumbnail-service
eventBus.subscribe('VideoTranscoded', async (event) => {
  const thumbnail = await generateThumbnail(event.transcodedUrl);
  await eventBus.publish('ThumbnailGenerated', {
    videoId: event.videoId,
    thumbnailUrl: thumbnail.url
  });
});

// notification-service
eventBus.subscribe('ThumbnailGenerated', async (event) => {
  await notifyUser(event.videoId, 'Video ready!');
});

Benefit: User không phải đợi 10 phút. Upload → Return ngay → Process background.

Use Case 3: Data Synchronization

Scenario: User update profile → Sync đến nhiều services

Event-Driven:

// user-service
await eventBus.publish('UserProfileUpdated', {
  userId: user.id,
  changes: { name: 'New Name', avatar: 'new-avatar.jpg' }
});

// order-service (cache user info)
eventBus.subscribe('UserProfileUpdated', async (event) => {
  await Order.updateMany(
    { userId: event.userId },
    { $set: { 'user.name': event.changes.name } }
  );
});

// review-service (cache user info)
eventBus.subscribe('UserProfileUpdated', async (event) => {
  await Review.updateMany(
    { userId: event.userId },
    { $set: { 'author.name': event.changes.name } }
  );
});

Khi nào KHÔNG NÊN dùng Events?

Use Case 1: Cần response ngay

Scenario: Check product availability

Bad (Event-Driven):

// Client request
await eventBus.publish('CheckInventory', { productId: 123 });

// ... đợi response? Làm sao?

Good (Request-Response):

const available = await fetch('http://inventory-service/check/123')
  .then(r => r.json());

if (available) {
  // Add to cart
}

Use Case 2: Cần transaction

Scenario: Transfer money giữa 2 accounts

Bad (Event-Driven):

// Deduct from account A
await eventBus.publish('MoneyDeducted', { accountId: 'A', amount: 100 });

// Add to account B
await eventBus.publish('MoneyAdded', { accountId: 'B', amount: 100 });

// Vấn đề: Nếu MoneyAdded fail → Money mất

Good (Request-Response với Transaction):

await db.transaction(async (trx) => {
  await Account.decrement('A', 100, { transaction: trx });
  await Account.increment('B', 100, { transaction: trx });
});

Use Case 3: Simple CRUD

Scenario: Get user by ID

Bad (Event-Driven): Overkill

Good (Request-Response):

const user = await fetch('http://user-service/users/123').then(r => r.json());

Implementation: Kafka vs RabbitMQ

Kafka

Best for:

  • High throughput (millions events/second)
  • Event streaming
  • Event sourcing
  • Log aggregation

Setup:

# docker-compose.yml
version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  
  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

Producer:

const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'order-service',
  brokers: ['localhost:9092']
});

const producer = kafka.producer();

await producer.connect();
await producer.send({
  topic: 'order-events',
  messages: [
    {
      key: order.id,
      value: JSON.stringify({
        type: 'OrderPlaced',
        data: order
      })
    }
  ]
});

Consumer:

const consumer = kafka.consumer({ groupId: 'email-service' });

await consumer.connect();
await consumer.subscribe({ topic: 'order-events' });

await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value.toString());
    
    if (event.type === 'OrderPlaced') {
      await sendOrderConfirmation(event.data);
    }
  }
});

RabbitMQ

Best for:

  • Complex routing
  • Priority queues
  • Request-reply patterns
  • Lower throughput requirements

Setup:

# docker-compose.yml
version: '3'
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"  # Management UI

Publisher:

const amqp = require('amqplib');

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

await channel.assertExchange('order-events', 'fanout', { durable: true });

channel.publish(
  'order-events',
  '',
  Buffer.from(JSON.stringify({
    type: 'OrderPlaced',
    data: order
  }))
);

Consumer:

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

await channel.assertExchange('order-events', 'fanout', { durable: true });
await channel.assertQueue('email-service-queue', { durable: true });
await channel.bindQueue('email-service-queue', 'order-events', '');

channel.consume('email-service-queue', async (msg) => {
  const event = JSON.parse(msg.content.toString());
  
  if (event.type === 'OrderPlaced') {
    await sendOrderConfirmation(event.data);
    channel.ack(msg);
  }
});

Patterns quan trọng

Pattern 1: Event Versioning

Vấn đề: Event schema thay đổi → Old consumers break

Solution:

// Version 1
{
  type: 'OrderPlaced',
  version: 1,
  data: {
    orderId: '123',
    total: 100
  }
}

// Version 2 (thêm field)
{
  type: 'OrderPlaced',
  version: 2,
  data: {
    orderId: '123',
    total: 100,
    currency: 'USD'  // New field
  }
}

// Consumer handle cả 2 versions
eventBus.subscribe('OrderPlaced', async (event) => {
  if (event.version === 1) {
    // Handle v1
    const currency = 'USD'; // Default
  } else if (event.version === 2) {
    // Handle v2
    const currency = event.data.currency;
  }
});

Pattern 2: Idempotency

Vấn đề: Event được process 2 lần → Duplicate actions

Solution:

eventBus.subscribe('OrderPlaced', async (event) => {
  // Check if already processed
  const processed = await ProcessedEvents.findOne({ eventId: event.id });
  if (processed) {
    console.log('Event already processed, skipping');
    return;
  }
  
  // Process event
  await sendOrderConfirmation(event.data);
  
  // Mark as processed
  await ProcessedEvents.create({ eventId: event.id, processedAt: new Date() });
});

Pattern 3: Dead Letter Queue

Vấn đề: Event processing fail → Event bị mất

Solution:

eventBus.subscribe('OrderPlaced', async (event) => {
  try {
    await processOrder(event);
  } catch (error) {
    // Retry 3 lần
    if (event.retryCount < 3) {
      event.retryCount = (event.retryCount || 0) + 1;
      await eventBus.publish('OrderPlaced', event);
    } else {
      // Move to Dead Letter Queue
      await eventBus.publish('DeadLetterQueue', {
        originalEvent: event,
        error: error.message,
        timestamp: new Date()
      });
    }
  }
});

Monitoring & Debugging

1. Event Tracing

// Add correlation ID
await eventBus.publish('OrderPlaced', {
  correlationId: uuidv4(),  // Track across services
  orderId: order.id,
  timestamp: new Date()
});

// Log in consumers
eventBus.subscribe('OrderPlaced', async (event) => {
  console.log(`[${event.correlationId}] Processing OrderPlaced`);
  await processOrder(event);
  console.log(`[${event.correlationId}] OrderPlaced processed`);
});

2. Metrics

const prometheus = require('prom-client');

const eventsPublished = new prometheus.Counter({
  name: 'events_published_total',
  help: 'Total events published',
  labelNames: ['event_type']
});

const eventsProcessed = new prometheus.Counter({
  name: 'events_processed_total',
  help: 'Total events processed',
  labelNames: ['event_type', 'status']
});

// Publish
await eventBus.publish('OrderPlaced', event);
eventsPublished.inc({ event_type: 'OrderPlaced' });

// Consume
try {
  await processOrder(event);
  eventsProcessed.inc({ event_type: 'OrderPlaced', status: 'success' });
} catch (error) {
  eventsProcessed.inc({ event_type: 'OrderPlaced', status: 'failure' });
}

Kết luận

Event-Driven Architecture mạnh mẽ nhưng phức tạp.

Dùng Events khi:

  • Fan-out (1 event → nhiều actions)
  • Long-running processes
  • Data synchronization
  • Decoupling critical

Dùng API calls khi:

  • Cần response ngay
  • Cần transaction
  • Simple CRUD
  • Strong consistency required

Lời khuyên:

  • Start với Request-Response
  • Add Events khi thấy pain points (tight coupling, cascading failures)
  • Không cần Events cho mọi communication
  • Hybrid approach thường tốt nhất

Tools:

  • Kafka: High throughput, event streaming
  • RabbitMQ: Complex routing, lower throughput
  • AWS SNS/SQS: Managed, easy setup
  • Google Pub/Sub: Managed, global scale

Event-Driven không phải silver bullet. Nhưng khi dùng đúng chỗ, nó transform architecture.

quizQuick Quiz
Câu 1/3

Sự khác biệt chính giữa Event-Driven và Request-Response là gì?

history_edu Góc học tập & giải trí

Thử Thách Kiến Thức Lịch Sử?

Khám phá hàng trăm câu hỏi trắc nghiệm lịch sử thú vị tại HistoQuiz. Vừa học vừa chơi, nâng cao kiến thức ngay hôm nay!

Chơi Ngay arrow_forward