IAnswerable

When Event-Driven Architecture Pays Off

14 Jul 2026

Event-driven architecture has a real cost — you trade “read the code top to bottom” for “trace a chain of handlers across services and hope your tracing is good enough.” That cost is worth paying in exactly three situations. Outside of them, it’s complexity you bought for a problem you don’t have.

Signal 1: Multiple, unrelated consumers of the same fact

When “a user signed up” needs to trigger a welcome email, a CRM sync, an analytics event, and a Slack notification to sales — and that list keeps growing — direct function calls start to rot. Every new consumer means editing the signup code path again:

// this keeps growing every time someone adds a new "thing that happens on signup"
async function signUp(data) {
  const user = await createUser(data)
  await sendWelcomeEmail(user)
  await syncToCRM(user)
  await trackSignupEvent(user)
  await notifySlack(user)   // added last week
  await enrollInOnboardingSequence(user) // added yesterday
  return user
}

The signup function now has to know about CRM syncing and Slack notifications, which have nothing to do with signing a user up. An event flips the dependency:

async function signUp(data) {
  const user = await createUser(data)
  events.emit('user.signed_up', { userId: user.id })
  return user
}

New consumers subscribe independently. The signup path stays small and stops changing every time marketing wants a new integration.

Signal 2: Consumers that genuinely shouldn’t block the producer

If sending a welcome email takes 3 seconds because of a slow email provider, should the user wait 3 extra seconds for their signup request to complete? No — that work should happen async, decoupled from the response the user is waiting on. This is a real, mechanical reason for a queue, not a stylistic preference: synchronous work that doesn’t need to be synchronous is just added latency.

Signal 3: Independent teams that need to stop coordinating deploys

If the billing team has to ask the notifications team to deploy a new handler before they can update their own signup flow, you’ve built coordination cost into a change that shouldn’t need it. Events let subscribing teams evolve independently — the billing team emits, notifications changes what it does with the event on its own schedule, without a joint deploy.

This only pays off when the teams are actually separate, with separate deploy cadences. If it’s three engineers on one team, this “benefit” is pure overhead — they were going to coordinate in the same standup either way.

What you give up

Debuggability. A stack trace stops at the event emit. Finding out what happened after “user.signed_up” fired means checking every subscriber, or having distributed tracing already in place, which is its own investment.

Ordering guarantees you have to think about explicitly. If “enroll in onboarding” needs to happen after “CRM sync” for some downstream reason, that ordering is no longer implicit in call order — it has to be designed into the event system deliberately, and it’s easy to get subtly wrong.

A failure mode with more moving parts. What happens when a subscriber is down when the event fires? Retried? Dead-lettered? Silently dropped? A direct function call fails loudly and immediately. An event can fail quietly, three services away from where anyone’s looking.

The check before you adopt it

Ask: do we have multiple genuinely independent consumers today, not hypothetically next quarter? Is any of this work actually latency-sensitive to decouple? Are the teams involved actually separate with separate deploy schedules? If you can only answer yes to “it feels like the more scalable architecture,” that’s not a yes — direct function calls, in a single well-structured module, will serve you better and debug easier until one of the three real signals shows up.