DEV Community

Cover image for Stop Making Every Backend Request Synchronous: Event-Driven Architecture in 2026
Subhadip Jana
Subhadip Jana

Posted on

Stop Making Every Backend Request Synchronous: Event-Driven Architecture in 2026

Modern backends are increasingly moving beyond the simple:

Request → API → Database → Response
Enter fullscreen mode Exit fullscreen mode

pattern.

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

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

publish an event:

await eventBus.publish({
  type: "OrderCreated",
  orderId,
  userId,
});
Enter fullscreen mode Exit fullscreen mode

Other services react independently:

                 ┌── Payment Service
                 │
Order Service ───┼── Inventory Service
                 │
                 ├── Email Service
                 │
                 └── Analytics Service
Enter fullscreen mode Exit fullscreen mode

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

A stream is more about:

“This happened.”

await stream.publish({
  type: "OrderCreated",
  orderId: "123",
  timestamp: Date.now()
});
Enter fullscreen mode Exit fullscreen mode

A useful mental model:

Queue  = Work to do

Stream = History of what happened
Enter fullscreen mode Exit fullscreen mode

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

Then:

events.subscribe("OrderCreated", async (event) => {
  await reserveInventory(event.orderId);
});
Enter fullscreen mode Exit fullscreen mode

And another consumer:

events.subscribe("OrderCreated", async (event) => {
  await generateInvoice(event.orderId);
});
Enter fullscreen mode Exit fullscreen mode

And another:

events.subscribe("OrderCreated", async (event) => {
  await sendConfirmationEmail(event.orderId);
});
Enter fullscreen mode Exit fullscreen mode

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

If the event arrives twice:

OrderCreated
     ↓
Payment
     ↓
Payment again 😬
Enter fullscreen mode Exit fullscreen mode

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

Now:

Event #abc
   ↓
Process
   ↓
Record #abc

Event #abc again
   ↓
Already processed
   ↓
Ignore
Enter fullscreen mode Exit fullscreen mode

That's a small pattern with a huge production impact.


Backpressure Is the Quiet Superpower

Imagine your API receives:

10,000 requests/sec
Enter fullscreen mode Exit fullscreen mode

but your email service can process only:

1,000 emails/sec
Enter fullscreen mode Exit fullscreen mode

Without buffering:

API
 ↓
Email Service
 ↓
💥 overload
Enter fullscreen mode Exit fullscreen mode

With a queue:

API
 ↓
Queue
 ↓
Worker
 ↓
Email Service
Enter fullscreen mode Exit fullscreen mode

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

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

into:

HTTP
 ↓
Event
 ↓
Queue
 ↓
Consumer
 ↓
Database
 ↓
Another Event
 ↓
HTTP
Enter fullscreen mode Exit fullscreen mode

if 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?
        ↓
       Stream
Enter fullscreen mode Exit fullscreen mode

The Architecture I'd Actually Build

For a modern application:

                 ┌──────────────┐
                 │   Frontend   │
                 └──────┬───────┘
                        │
                        ▼
                 ┌──────────────┐
                 │     API      │
                 └──────┬───────┘
                        │
                 ┌──────▼───────┐
                 │   Database   │
                 └──────────────┘
                        │
                        ▼
                  ┌───────────┐
                  │ Event Bus │
                  └─────┬─────┘
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
      Payments      Analytics       Emails
          │             │             │
          ▼             ▼             ▼
       Worker         Worker         Worker
Enter fullscreen mode Exit fullscreen mode

The 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.

Top comments (0)