DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Event-Driven Architecture: When to Use Events vs Direct Calls

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;
}
Enter fullscreen mode Exit fullscreen mode

When Events Help

Use events when:

  1. Multiple consumers need to react to one action
  2. The producer shouldn't wait for consumers (async side effects)
  3. Consumers are optional — business logic shouldn't fail if they do
  4. 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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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)