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.
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:
- User upload video
- Transcode video (mất 5-10 phút)
- Generate thumbnail
- Extract metadata
- 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.
Sự khác biệt chính giữa Event-Driven và Request-Response là gì?
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!