Modern backends are increasingly moving beyond the simple:
Request → API → Database → Responsepattern.
This works beautifully—until one request needs to trigger five other operations.
Imagine:
POST /orders
↓
Create order
↓
Charge payment
↓
Reserve inventory
↓
Generate invoice
↓
Send email
↓
Update analyticsIf every operation happens synchronously, one slow dependency can make the entire request slow or fail.
That's where event-driven architecture (EDA) becomes useful.
Recent 2026 backend engineering coverage continues to highlight event-driven systems, queues, streams, serverless consumers, and asynchronous processing as important patterns for scalable applications.
The Basic Idea
Instead of making the order service call everything directly:
await createOrder();
await chargePayment();
await reserveInventory();
await generateInvoice();
await sendEmail();publish an event:
await eventBus.publish({
type: "OrderCreated",
orderId,
userId,
});Other services react independently:
┌── Payment Service
│
Order Service ───┼── Inventory Service
│
├── Email Service
│
└── Analytics ServiceThe producer doesn't need to know how every consumer works.
Queue vs Stream
One of the most important distinctions is queue vs stream.
A queue is usually about:
“Please process this job.”
await queue.send({
type: "GENERATE_INVOICE",
orderId: "123"
});A stream is more about:
“This happened.”
await stream.publish({
type: "OrderCreated",
orderId: "123",
timestamp: Date.now()
});A useful mental model:
Queue = Work to do
Stream = History of what happenedQueues are excellent for background jobs, buffering traffic spikes, retries, and task distribution. Streams become valuable when multiple consumers need the same event, replayability matters, or event history itself is useful.
The Backend Becomes Asynchronous
A simple Node.js API might become:
app.post("/orders", async (req, res) => {
const order = await createOrder(req.body);
await events.publish({
type: "OrderCreated",
orderId: order.id,
});
res.status(201).json(order);
});Then:
events.subscribe("OrderCreated", async (event) => {
await reserveInventory(event.orderId);
});And another consumer:
events.subscribe("OrderCreated", async (event) => {
await generateInvoice(event.orderId);
});And another:
events.subscribe("OrderCreated", async (event) => {
await sendConfirmationEmail(event.orderId);
});Now these operations don't need to block the original HTTP request.
But There's a Catch: Duplicate Events
Event-driven systems commonly need to tolerate at-least-once delivery, meaning a consumer can receive the same event more than once.
So this is dangerous:
async function handlePayment(event) {
await chargeCard(event.orderId);
}If the event arrives twice:
OrderCreated
↓
Payment
↓
Payment again 😬Instead, make consumers idempotent.
async function handlePayment(event) {
const alreadyProcessed =
await db.processedEvents.findUnique({
where: {
eventId: event.id
}
});
if (alreadyProcessed) {
return;
}
await chargeCard(event.orderId);
await db.processedEvents.create({
data: {
eventId: event.id
}
});
}Now:
Event #abc
↓
Process
↓
Record #abc
Event #abc again
↓
Already processed
↓
IgnoreThat's a small pattern with a huge production impact.
Backpressure Is the Quiet Superpower
Imagine your API receives:
10,000 requests/secbut your email service can process only:
1,000 emails/secWithout buffering:
API
↓
Email Service
↓
💥 overloadWith a queue:
API
↓
Queue
↓
Worker
↓
Email ServiceThe queue absorbs the spike.
Workers can then process jobs at a sustainable rate:
const worker = new Worker({
concurrency: 20,
async process(job) {
await sendEmail(job.data);
}
});This is backpressure: producers can move faster than consumers without immediately bringing the whole system down.
Don't Turn Everything Into Events
This is probably the most important lesson.
Event-driven architecture isn't automatically better.
Don't turn:
const user = await getUser(id);into:
HTTP
↓
Event
↓
Queue
↓
Consumer
↓
Database
↓
Another Event
↓
HTTPif the caller simply needs a user immediately.
Synchronous request/response remains the right choice when the caller genuinely needs the result in the same interaction. Event-driven architecture earns its complexity when producers and consumers need to scale, deploy, or fail independently.
A good rule:
Need the result now?
↓
Synchronous
Can the work happen later?
↓
Asynchronous
Need multiple independent consumers?
↓
Event
Need durable event history/replay?
↓
StreamThe Architecture I'd Actually Build
For a modern application:
┌──────────────┐
│ Frontend │
└──────┬───────┘
│
▼
┌──────────────┐
│ API │
└──────┬───────┘
│
┌──────▼───────┐
│ Database │
└──────────────┘
│
▼
┌───────────┐
│ Event Bus │
└─────┬─────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Payments Analytics Emails
│ │ │
▼ ▼ ▼
Worker Worker WorkerThe API stays fast.
Workers scale independently.
Failures are isolated.
And expensive work doesn't have to occupy an HTTP request.
Final Thought
The future of backend engineering isn't simply:
“Use microservices.”
It's more nuanced:
Choose synchronous communication when you need an immediate answer, and asynchronous events when you need independent work, resilience, fan-out, or scale.
The interesting engineering challenge isn't adding Kafka, RabbitMQ, or another message broker.
It's knowing when you don't need one.
Good architecture isn't about adding more infrastructure.
It's about adding exactly enough infrastructure to solve the problem.
