Event-Driven Architecture: When to Use Events vs Direct Calls
Event-driven architecture decouples producers from consumers. It's powerful — and overused. Here's when it actually makes sense.
Direct Calls: The Default
For most operations, direct function calls are correct:
// Direct call: simple, synchronous, traceable
async function createOrder(data: CreateOrderDto) {
const order = await db.orders.create({ data });
await sendOrderConfirmationEmail(order); // Direct call
await updateInventory(order); // Direct call
return order;
}
When Events Help
Use events when:
- Multiple consumers need to react to one action
- The producer shouldn't wait for consumers (async side effects)
- Consumers are optional — business logic shouldn't fail if they do
- Cross-service communication in a distributed system
In-Process Event Bus
import EventEmitter from 'eventemitter3';
export const eventBus = new EventEmitter();
// Producer: emit and move on
async function createOrder(data: CreateOrderDto) {
const order = await db.orders.create({ data });
// Fire and forget — consumers handle asynchronously
eventBus.emit('order.created', order);
return order; // Returns immediately without waiting for side effects
}
// Consumer 1: email
eventBus.on('order.created', async (order: Order) => {
await sendOrderConfirmationEmail(order);
});
// Consumer 2: analytics
eventBus.on('order.created', async (order: Order) => {
await trackRevenue(order);
});
// Consumer 3: inventory
eventBus.on('order.created', async (order: Order) => {
await updateInventory(order.items);
});
Durable Events: Queues
In-process events are lost on crash. For durability, use a queue:
import Queue from 'bull';
const orderQueue = new Queue('orders', process.env.REDIS_URL);
// Producer
async function createOrder(data: CreateOrderDto) {
const order = await db.orders.create({ data });
await orderQueue.add('order.created', order, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
return order;
}
// Consumer
orderQueue.process('order.created', async (job) => {
await sendOrderConfirmationEmail(job.data);
});
The Right Pattern Per Scenario
| Scenario | Pattern |
|---|---|
| Email after signup | In-process event |
| Payment processing | Direct call (must succeed) |
| Analytics tracking | Fire-and-forget event |
| Cross-service workflow | Durable queue |
| Real-time notifications | WebSocket/SSE + event |
Event-driven patterns, BullMQ job queues, and async workflow infrastructure are production-ready in the AI SaaS Starter Kit.
Top comments (0)