Design Desk

Architecture Patterns

Event-Driven Architecture

Decoupling who reacts to something happening from the service where it happened, and why that same decoupling makes 'what actually happened, in order' the hardest question to answer when something breaks.

Architecture PatternsIntermediate7 min readPrerequisites: Microservices Architecture

After this lesson, you should be able to:

  • Compare direct service-to-service calls with publish/subscribe events, and what each does to coupling between publisher and subscriber.
  • Explain why adding a new interested service is free under pub/sub but requires changing the publisher under direct calls.
  • Diagnose why a silently-failing subscriber can go unnoticed, unlike a failed direct call that fails loudly at the call site.
  • Recognize the cost pub/sub trades for that decoupling: a request's full flow is no longer visible in one call stack.

At a Glance

  • A service that directly calls every other service interested in something happening has to change its own code every time a new service wants to know, and know about all of them by name.
  • Publishing an event once and letting interested services subscribe removes that coupling: the publisher doesn't need to know who's listening or how many there are.
  • The cost is debugging. There's no longer one call stack to read. Tracing what actually happened means reconstructing it after the fact from whichever services reacted, and whichever didn't.

01 The Problem

An order service, when an order is placed, calls shipping's API to create a shipment, calls billing's API to charge the card, and calls analytics' API to log the event, three direct calls, hardcoded into the order service's code, which now knows about three other services by name and URL. Six months later, fraud wants to know about every order too, to run a real-time risk check. Adding them means opening the order service's code, wiring in a fourth call, testing that it doesn't break the other three, and redeploying the order service, even though nothing about placing an order actually changed. The order team, who doesn't build fraud detection, now owns that change.

There's a second problem hiding in the first one. If the order service calls analytics synchronously and analytics is slow or down, does placing an order fail because an internal logging service is having a bad day? Nobody buying a shirt should have their order rejected for that. But if the order service just fires the call and moves on without waiting, and that call fails silently, analytics never gets the event and nobody notices until the numbers don't add up at the end of the month.

Publishing an "order placed" event once, to something every interested service can subscribe to, removes the order service's need to know any of them exist. It just says what happened. Whoever cares, listens. This is event-driven architecture, and the cost is baked into that same sentence: whoever cares listens is exactly what makes it hard to later answer what actually happened, in what order, and whether everyone who needed to see it actually did.

02 What It Is

Event-driven architecture structures a system around services publishing events, facts about something that already happened, "order placed," "payment failed," "shipment created", to a shared channel, and other services subscribing to whichever events they care about and reacting independently. The publisher doesn't call subscribers directly and doesn't know how many exist or what they do with the event. This usually runs on a message queue or an event streaming platform as the transport, since durability and at-least-once delivery matter here the same way they matter for a single point-to-point queue.

Removing the publisher's knowledge of its subscribers is also removing your ability to read the flow in one place. In a direct-call system, a stack trace or a sequence diagram shows exactly what happened and in what order, one call leading to the next. In an event-driven system, "what happened when order 4471 was placed" is scattered across however many services reacted, each with its own logs, its own timing, and a possible failure the publisher never sees and never blocks on. Debugging trades a stack trace for detective work.

In Plain Terms

A store manager who personally calls the baker, the florist, and the caterer every time a wedding is booked has to remember all three phone numbers and make all three calls herself, and if the venue decorator wants to be looped in too, she has to remember to call them as well, every time, forever. Posting the booking on a shared board that the baker, florist, caterer, and decorator each check on their own removes her from the loop entirely, a fifth vendor can start checking the board without her ever finding out they exist. What she loses: she can no longer tell you, just by remembering her own calls, whether the florist actually saw the booking or the flowers are simply not going to show up.

03 How It Works

Rendering diagram…

Every one of these calls is hardcoded into the order service. Adding a fourth interested service means changing the order service's code, whether or not the order team is the one who wants that change.

The other side of that coin, publish once and let whoever cares subscribe, is easier to show than to draw. Try it: publish an event with all three subscribers enabled, then disable Analytics and publish again. Shipping and Billing still react normally. Analytics shows nothing different at all, no error, no red flag, it just quietly doesn't move. That silence is the actual cost this article is about.

Events published: 0

Shipping

Idle

Received: 0

Billing

Idle

Received: 0

Analytics

Idle

Received: 0

No events published yet.

04 Trade-offs

 Direct service callsEvents (pub/sub)
Adding a new interested serviceRequires changing and redeploying the calling service's codeNew service just subscribes; the publisher's code never changes
CouplingPublisher knows every subscriber by name and APIPublisher knows nothing about who's listening or how many there are
Tracing a requestOne call stack or sequence diagram shows the whole flow, in orderThe flow is scattered across however many services reacted, each on its own timeline
Failure visibilityA failed call fails loudly, at the call site, immediatelyA subscriber that silently fails to process an event can go unnoticed until its absence is felt downstream

Watch out

It's tempting to treat "events" as strictly better than direct calls: fewer dependencies, more flexibility, obviously more scalable. Decoupling who reacts from where it happened is real, but it's not free, it's a trade for the one thing direct calls give you for free: a linear trail of what happened. An event-driven system with several services silently reacting to the same event has no single place showing you all of them happened, or in what order, or whether one quietly never ran. You're trading a coupling problem for an observability problem, and if you don't build the observability, you'll find out about it during an incident, not before.

05 Where You'll Actually See This

The payment gateway case study's asynchronous webhook is a narrow version of this: the processor tells the gateway a charge settled, without the processor knowing or caring what the gateway does in response. Event-driven architecture is that same idea generalized from one publisher and one subscriber to one publisher and however many subscribers care.

Message queues is usually the actual transport underneath this: durable delivery, at-least-once semantics, and redelivery on a failed consumer all apply here exactly as they do for a single point-to-point queue. The difference is one message can have many independent consumers instead of one. Microservices architecture already raises the problem this solves: once a system is split into services, something has to coordinate them, and direct API calls are one way, wiring every caller to every callee it depends on. Events are the other: the caller announces what happened and stops caring who's listening.

One common subscriber is worth naming specifically: CQRS's read model is often updated by subscribing to exactly this kind of event. The write side publishes what happened, and whatever's building the read model listens and updates its own copy, the same lag this article already describes, just applied to keeping a denormalized read model in sync instead of notifying a separate service.

This whole article has been assuming the publish itself just happens. It doesn't, reliably, on its own: writing to a database and publishing an event are two separate operations, and the outbox pattern is what closes the gap between them so "publishes an event" is actually true instead of something that can silently fail.

How to reason about it

When asked to justify an event-driven design, don't stop at "it's more decoupled." Say what you're giving up to get it: a linear trail of what happened, replaced by however many services reacted independently. If your answer doesn't also cover how you'd trace one event through every consumer that reacted to it, correlation IDs, an event log you can replay, you've described the coupling win without the debugging cost that comes with it.

06 Quick Check

An 'order placed' event gets published and three services subscribe: shipping, billing, and loyalty. Loyalty's consumer has a bug and silently fails to process every event for two weeks. What's the most likely way this actually gets discovered?