Event-Driven Architecture: Why Your App Needs It
Have you ever clicked "Place Order" on a website and wondered — what exactly happens in that split second before the confirmation page loads?
Behind the scenes, a surprisingly large amount of work might be triggered: your payment gets charged, a receipt email is sent, inventory is updated, a warehouse notification goes out, and maybe a loyalty points calculation runs. If your server tried to do all of that before responding to you, you'd be staring at a loading spinner for several seconds. Worse — if any one of those steps crashed, your entire order might fail.
This is the problem that event-driven architecture solves. And it's the foundation of everything you'll learn in this module.
Let's discover how it works — and why it matters more than you might think.
Quick Reference
What it is: A design pattern where work is triggered by events rather than done immediately inside a request.
Core idea: Decouple "something happened" from "what we do about it."
When to use it: Anytime work is slow, optional, or can happen after you've already responded to the user.
What it replaces: Doing everything synchronously inside a single HTTP request.
What You Need to Know First
This is a foundational article — you don't need any prior experience with queues, message brokers, or distributed systems to follow along.
You should be comfortable with:
- Basic web concepts: What an HTTP request and response are (a browser asks, a server answers)
- JavaScript/TypeScript basics: Functions, async/await, and reading simple code samples
- What an API is: A way for software to talk to other software
If any of those feel shaky, I would recommend spending 30 minutes with a basic JavaScript async/await tutorial first.
What We'll Cover in This Article
By the end of this guide, you'll understand:
- Why the synchronous request-response model has limits
- What "event-driven" actually means in plain terms
- The three core concepts: events, producers, and consumers
- The difference between synchronous and asynchronous execution
- Real-world scenarios where event-driven architecture wins
- What this has to do with Inngest specifically
What We'll Explain Along the Way
Don't worry if you're unfamiliar with these — we'll define them as we go:
- Background jobs (what they are and why they exist)
- Message queues (a brief, intuitive introduction)
- Producers and consumers (with a concrete analogy)
- The "critical path" (an important mental model for performance)
The Story That Makes Everything Click
Let's start with a story. It'll feel familiar.
Imagine you've just built a user signup flow for your app. When someone registers, your server needs to:
- Save the user to the database
- Send a welcome email
- Provision a free trial in your billing system
- Post a notification to your team's Slack channel
- Log the event to your analytics platform
Here's the straightforward version of that code:
// POST /api/signup
async function handleSignup(req: Request, res: Response) {
const { email, name } = req.body;
// Step 1: Save user
const user = await db.users.create({ email, name });
// Step 2: Send welcome email (takes ~800ms)
await emailService.sendWelcome(user);
// Step 3: Set up billing trial (takes ~1200ms)
await billingService.createTrial(user.id);
// Step 4: Notify Slack (takes ~400ms)
await slack.notify(`New signup: ${email}`);
// Step 5: Log to analytics (takes ~300ms)
await analytics.track("user_signed_up", { userId: user.id });
// Only NOW do we respond
res.json({ success: true, userId: user.id });
}
What's wrong here?
The user is waiting. Not for step 1 — that's essential, they need an account. They're also waiting for steps 2, 3, 4, and 5 — none of which they can see or feel. The welcome email will arrive in their inbox moments later regardless. The Slack notification is for your team, not them. The analytics call is for your dashboards.
Let's add up the wait time: 800ms + 1200ms + 400ms + 300ms = 2,700ms of extra delay. Nearly three seconds of the user staring at a spinner, waiting for things that aren't actually for them.
And then there's the fragility problem. What happens if the billing API is having a slow day and times out? Does the signup fail? Does the user get an error? Maybe they retry, creating a duplicate account. Maybe they give up entirely.
You might be wondering: "Couldn't I just use Promise.all() to run them in parallel?" Great instinct — let's explore that:
// Parallel approach - faster, but still fragile
await Promise.all([
emailService.sendWelcome(user),
billingService.createTrial(user.id),
slack.notify(`New signup: ${email}`),
analytics.track("user_signed_up", { userId: user.id }),
]);
Better! Now instead of 2,700ms, you're waiting for the slowest one: ~1,200ms. But the fragility is still there. If the billing service throws an error, Promise.all() rejects the entire batch. The user still gets a failure response — even though their account was created successfully.
Here's the key insight: All of those extra steps are not part of the core act of signing up. The user is signed up the moment their row is saved in the database. Everything else is a reaction to that event.
This is the central idea of event-driven architecture.
What "Event-Driven" Actually Means
Let's define our terms clearly, because "event-driven" gets used loosely.
What is an event?
An event is a record that something meaningful happened. Not a request for something to happen — a notification that it already did.
Some examples of events:
user.signed_up— a new user created an accountorder.placed— a customer submitted an orderpayment.failed— a charge was declinedfile.uploaded— a user uploaded a documentsubscription.cancelled— a user cancelled their plan
Notice the naming convention: past tense. Events describe facts that have already occurred, not instructions for what to do next. This distinction matters — and we'll come back to it.
An event typically carries data about what happened. For example:
// The event itself - a record of something that happened
const event = {
name: "user.signed_up",
data: {
userId: "usr_01J8G4470",
email: "alex@example.com",
name: "Alex",
signedUpAt: "2025-03-15T14:22:00Z",
plan: "free_trial",
},
};
The event doesn't say "send a welcome email." It just says "a user signed up, here's the data." What happens because of that event is handled elsewhere — by consumers.
The three actors: producers, channels, and consumers
Every event-driven system has three roles, as described by TechTarget's overview of EDA:
Producer — The part of your system that detects something happened and emits the event. In our signup example, the API endpoint is the producer. It saves the user and then fires a user.signed_up event.
Channel — The medium through which the event travels. This might be a message queue, an event bus, or a platform like Inngest. It receives the event from the producer and holds onto it until consumers are ready to process it.
Consumer — A function or service that listens for specific events and reacts to them. You might have three consumers for user.signed_up: one that sends the welcome email, one that sets up billing, and one that posts to Slack.
Here's what this looks like visually — the same signup flow, rewritten as an event-driven system:
[User submits form]
↓
[API: Save user to DB] ← This is the only synchronous step
↓
[API: Fire event: "user.signed_up"]
↓
[API: Respond to user immediately: ✅ "You're signed up!"]
Meanwhile, in the background...
↓
[Consumer 1: Send welcome email]
[Consumer 2: Set up billing trial]
[Consumer 3: Notify Slack]
[Consumer 4: Log to analytics]
The user gets their confirmation in milliseconds. The rest of the work happens asynchronously, in the background, at its own pace.
Synchronous vs. Asynchronous: The Core Mental Model
Let's make this concrete with an analogy.
Synchronous is like calling a restaurant to order takeout and staying on the phone while they cook. You can't do anything else. You just wait. When the food is ready, they tell you — and only then can you hang up and go pick it up.
Asynchronous is like ordering via an app. You place the order, the app confirms it, and you put your phone down. You can watch TV, take a shower, or read a book. When the food is ready, you get a notification. You weren't blocked — you were just informed.
In software terms:
| Synchronous | Asynchronous | |
|---|---|---|
| User waits for | Everything to finish | Only the essential step |
| If one step fails | The whole request fails | Other steps continue independently |
| User experience | Slower, potentially fragile | Fast response, resilient |
| Background work | Impossible | Natural |
The "critical path" is the series of steps a user has to wait for before getting a response. Event-driven architecture is about making the critical path as short as possible — doing only what's essential before responding, and deferring everything else.
A Real-World Example: E-Commerce Order Processing
Let's look at a more complex example. A customer places an order on an e-commerce site. What needs to happen?
Must happen before responding (critical path):
- Validate the cart (items are still in stock, prices are correct)
- Charge the payment method
- Create the order record in the database
Can happen after responding (background work):
- Send an order confirmation email
- Update inventory counts
- Notify the warehouse to pack the order
- Update the customer's purchase history
- Trigger loyalty points calculation
- Log the sale to your analytics system
In a synchronous system, all of this happens before the customer sees "Order confirmed!" — a painful wait, and a single point of failure.
In an event-driven system, the moment the payment succeeds and the order is created, you fire an order.placed event and respond immediately. Your background consumers handle the rest — independently, reliably, and at their own pace.
As Wikipedia's EDA entry puts it: an event is "a significant change in state." The state changed from "cart" to "order." Everything that follows is a reaction to that change.
Real companies use this at massive scale. Growin's 2025 architecture guide notes that Shopify handles roughly 66 million messages per second during peak traffic using event-driven patterns — impossible to achieve with synchronous processing alone.
Why This Architecture Wins
Let's look at the concrete benefits, one by one.
1. Users get faster responses
When you move background work off the critical path, your API responds faster. Instead of waiting 3 seconds for five sequential operations, you respond in 50ms after the essential step. Users feel the difference immediately.
2. Your system becomes more resilient
Imagine the email service goes down for 10 minutes. In a synchronous system, every signup fails during that window — even though the database is fine and the user's account was created. In an event-driven system, the user.signed_up event sits in a queue. When the email service recovers, it processes the backlog. No signups lost.
As ZeePalm's EDA guide explains, decoupling components through events means that "a failure in one service does not affect others." Each consumer fails and retries independently.
3. You can add new behaviour without touching old code
Want to add a new step to the signup flow — say, a personalized onboarding questionnaire email sent 24 hours after signup? In a synchronous system, you'd modify the signup handler, potentially breaking things. In an event-driven system, you create a new consumer that listens for user.signed_up and schedules a delayed email. The signup handler never changes.
This is the open/closed principle in action: your system is open to extension but closed to modification.
4. You can scale consumers independently
If your welcome email volume spikes (say, you just ran a big campaign), you can scale just the email consumer — not your entire API. Each consumer is an independent unit that can be tuned, scaled, and deployed separately.
5. You get a natural audit trail
Every event is a timestamped record of something that happened. If you store your events (which Inngest does by default), you have a complete history of your system's state changes. This is invaluable for debugging, compliance, and understanding user behaviour.
Common Misconceptions
❌ Misconception: Event-driven means your API has no control over what happens
Reality: Your API decides when to emit events and what data they carry. You're not giving up control — you're changing how control is expressed. Instead of calling functions directly, you describe what happened and let consumers react.
Why this matters: Some developers resist events because they feel like they're "losing visibility" into what happens next. In practice, event-driven systems often have more visibility because you can inspect every event that was emitted and every consumer that ran.
❌ Misconception: You need events for everything
Reality: Not every operation needs to be event-driven. Reading data from a database and returning it to the user? Keep that synchronous — it's fast and simple. Events shine when work is slow, optional, retryable, or needs to happen after you've responded.
// ✅ Keep this synchronous — fast, simple, no side effects
async function getUserProfile(userId: string) {
return await db.users.findById(userId);
}
// ✅ Make this event-driven — slow, has side effects, can fail independently
async function handleUserSignup(userData: UserData) {
const user = await db.users.create(userData);
await inngest.send({ name: "user.signed_up", data: { userId: user.id } });
return { success: true, userId: user.id };
}
❌ Misconception: "Firing an event" means the work is optional
Reality: Events are reliable — they're not fire-and-forget if you use the right tools. A platform like Inngest guarantees that every event is processed, retried on failure, and tracked. The work is asynchronous, not optional.
The Problem Event-Driven Architecture Doesn't Solve on Its Own
Here's something important to understand before we go further.
Event-driven architecture describes the pattern. It doesn't tell you how to:
- Retry a consumer that failed halfway through
- Pause execution and wait for another event before continuing
- Handle the case where a consumer runs twice due to a retry
- Inspect what happened when something went wrong
- Set up a development environment that mimics production
These are the operational challenges of event-driven systems. They're real, and they're why many teams build the pattern correctly in principle but struggle with it in practice.
This is precisely where Inngest fits in — and what the rest of this module is about. As Inngest's own pattern library describes, the platform handles concerns like moving work off the critical path, fan-out (one event triggering multiple functions), scheduling, and reliability — without you having to build that infrastructure yourself.
SoundCloud's CTO put it well in an Inngest post: the goal was to find a solution that would let the team "just write the code, not manage the infrastructure around queues, concurrency, retries, error handling, prioritization."
That's the promise of the tools we'll explore in this module.
Troubleshooting Common Misconceptions Before You Start
Problem: "I'm not sure if my use case needs events"
Ask these questions:
- Does the user need to wait for this work before they can proceed? If no → background job candidate.
- What happens if this step fails? If the answer is "only this step fails, not the whole request" → event-driven candidate.
- Could this work happen 5 seconds later with no user-facing impact? If yes → event-driven candidate.
Diagnostic tool:
// Mental model test
function shouldBeEventDriven(operation: string): boolean {
const isOnCriticalPath = userNeedsResultToProcceed(operation);
const hasIndependentFailureMode = canFailWithoutAffectingUser(operation);
const isTimeInsensitive = canHappenAsynchronously(operation);
// If it's not on the critical path AND it can fail independently → event-driven
return !isOnCriticalPath && (hasIndependentFailureMode || isTimeInsensitive);
}
Problem: "I'm worried about events getting lost"
Solution: Use a platform with durable event storage. Inngest persists every event it receives to a database. Even if your consumer function is down, the event is stored and will be retried when the consumer recovers. We'll cover retries, timeouts, and failure-handling patterns in a dedicated follow-up article.
Problem: "Debugging async code is harder than synchronous code"
This is true — and it's a real cost. The solution isn't to avoid async patterns; it's to use tools with good observability. Inngest's dashboard shows you every event, every function run, every step, and every error. We'll explore how to read and debug run logs in a dedicated observability article.
Check Your Understanding
Quick Quiz
1. In the signup example, which steps belong on the critical path (must happen before responding)?
Show Answer
Only saving the user to the database. Everything else — email, billing, Slack, analytics — can happen after you've responded. The user only needs confirmation that their account was created.
2. What's wrong with this approach?
async function handleOrderPlaced(orderId: string) {
// Fire an event
await events.emit("order.placed", { orderId });
// But also do the work right here, synchronously
await emailService.sendConfirmation(orderId);
await warehouse.notifyPickup(orderId);
}
Show Answer
This doubles the work. The synchronous calls (emailService.sendConfirmation, warehouse.notifyPickup) block the response, while the event will also trigger consumers that do the same work again. The result is duplicate emails, duplicate warehouse notifications, and a slower API. Either do the work synchronously or emit an event — not both.
3. Why are events named in the past tense (order.placed vs place.order)?
Show Answer
Events describe something that already happened — a state change. They're not commands or requests. order.placed says "an order was placed — here's the data." Consumers then decide what to do about it. Naming events in past tense keeps this distinction clear and prevents you from treating your event bus like a remote procedure call system.
Summary: Key Takeaways
- Synchronous processing means the user waits for every step to complete before getting a response. This is slow and fragile when you have many side effects.
- Event-driven architecture separates "something happened" from "what we do about it." Emit an event, respond immediately, and let background consumers handle the rest.
- An event is a past-tense record of a meaningful state change, with data describing what occurred.
- Producers emit events. Channels carry them. Consumers react to them.
- The critical path is what the user has to wait for. Keep it minimal. Move everything else to the background.
- Events don't mean unreliable. With the right platform, every event is guaranteed to be processed, retried, and tracked.
- Not everything needs events. Simple reads, fast lookups, and truly synchronous workflows don't benefit from this pattern.
What's Next?
Now that you understand why event-driven architecture exists, the next article will give you the vocabulary to talk about it fluently.
In the next article, we'll go deeper into each component: events, queues, and workers. You'll learn what a message queue really is, how workers process jobs, and how these pieces fit together into a system you can reason about — before we write a single line of Inngest code.
Version Information
Relevant tools and versions referenced:
- Node.js: v18.x, v20.x, v22.x
- TypeScript: 5.x
- Inngest SDK: referenced conceptually (installation covered in Article 4)
Further reading:
- Inngest Pattern Library — real-world patterns with examples
- Wikipedia: Event-Driven Architecture — academic definition and topology overview
- Inngest GitHub Repository — open source codebase and architecture walkthrough