Skip to content
North Tech Labs
ArchitecturePart of: Architecture & Systems Integration

Event-Driven Architecture: When and How to Use It

Event-driven architecture is a design style where services communicate by publishing facts about what happened — an order was placed, a payment cleared — rather than calling each other directly and waiting for a response. It fits systems where producers and consumers are deployed independently, where one action should trigger several unrelated reactions, or where a slow downstream step shouldn't block a fast upstream one. It is the wrong choice for most CRUD operations and anything that needs an immediate answer to show a user, because it trades immediate consistency and easy tracing for looser coupling and eventual consistency.

Key takeaways

  • Event-driven systems react to facts that already happened instead of issuing commands and waiting for a synchronous reply
  • The core payoff is loose coupling and natural one-to-many fan-out; the core cost is eventual consistency and harder tracing
  • Event notification, event-carried state transfer, and event sourcing are three different patterns with very different complexity budgets
  • Idempotent handlers are mandatory, not optional, because most brokers only guarantee at-least-once delivery
  • The strongest adoption path introduces events at one integration boundary at a time rather than rearchitecting the whole system around them

What "event-driven" actually means

An event-driven system communicates through facts, not requests. When something happens — a customer places an order, a sensor reports a reading, a document finishes processing — a service publishes an event describing that fact to a broker. Other services subscribe to the events they care about and react independently, on their own schedule. The publisher doesn't know who's listening, doesn't wait for a reply, and doesn't care whether zero, one, or ten consumers eventually act on what it announced.

This is a genuinely different communication model from the request-response pattern most engineers reach for first. In a synchronous API call, the caller names the specific service it wants, sends a command ("create this order"), and blocks until it gets an answer back — success, failure, or a timeout. The caller and callee are coupled in three ways: they must both be available at the same moment, the caller must know the callee's address, and the caller's own execution is paused until the callee responds.

Event-driven architecture breaks all three couplings deliberately. The publisher doesn't need the consumer to be running right now — the event can sit in a queue until the consumer catches up. The publisher doesn't need to know the consumer's identity or even that it exists — new consumers can subscribe to the same event stream later without the publisher changing anything. And the publisher isn't blocked waiting for the consumer's work to finish — it fires the event and moves on.

It's worth being precise about the vocabulary here, because "event" gets used loosely. An event is a statement about the past — OrderPlaced, PaymentCaptured, InventoryReserved — expressed as a fact, not an instruction. A command, by contrast, is a request for something to happen — PlaceOrder, CapturePayment — and it's typically directed at exactly one recipient who's expected to either do it or explicitly reject it. Confusing the two is one of the most common design mistakes in systems that call themselves event-driven: if your "event" has one specific intended recipient and expects a specific outcome, it's a command wearing an event's name, and the distinction matters because it changes what the sender is allowed to assume about delivery and response.

Why teams reach for this pattern

The appeal comes down to three concrete properties, each of which solves a specific pain point synchronous calls create at scale.

Loose coupling between producers and consumers. In a system built entirely on synchronous calls, adding a new capability that reacts to an existing action — send a welcome email when a user signs up, update a search index when a product changes, notify a partner system when a shipment ships — means modifying the original code path to add a new call. Every consumer added this way makes the original action slower and more fragile, because now it depends on the availability of every service it calls. With events, the signup service publishes UserRegistered once. The email service, the analytics pipeline, and the CRM sync each subscribe independently, and none of them required a single line of the signup service to change.

Natural fit for one-to-many fan-out. Some business processes are inherently broadcast in nature — one thing happens, and several unrelated parts of the system need to know about it for different reasons. Trying to model that as a chain of synchronous calls forces an arbitrary ordering and makes the initiating service responsible for knowing about every downstream consumer. An event published once, consumed by many, matches the actual shape of the problem.

Resilience to a consumer being temporarily unavailable. If a downstream service is deployed, restarting, or degraded, a synchronous caller either blocks, times out, or fails outright. With a durable queue in between, the event simply waits. The consumer catches up when it's healthy again, and nothing upstream needed to know there was ever a problem. This is a meaningfully different failure mode from a synchronous chain, where the availability of the whole path is bounded by the least available link.

These are real, well-earned benefits — but they are not free, and the costs are exactly the kind that don't show up until a system is already in production and something is going wrong at 2 a.m.

The costs nobody puts on the architecture diagram

Request flows become harder to trace. In a synchronous call chain, a stack trace or a distributed trace tool can show you the entire path a request took, service by service, because everything happened as one continuous chain of calls. In an event-driven flow, the "request" is really a sequence of independent reactions separated in time and possibly in the codebase entirely. Answering "what happened to order #4021" might require correlating log entries across five services and three queues, none of which have a shared call stack to inspect. Distributed tracing tools that propagate a correlation ID through event metadata help considerably, but they have to be deliberately built in — they don't happen automatically the way a synchronous stack trace does.

Consistency becomes eventual, not immediate. When service A calls service B synchronously and gets a 200 response, the caller knows B's state has been updated by the time it moves on. When A publishes an event and B consumes it asynchronously, there's a window — milliseconds under normal load, potentially much longer under backpressure or an outage — during which A's world has moved on but B hasn't caught up yet. This is fine for a huge number of business processes and actively wrong for others. A read-your-own-writes requirement (a user updates their profile and expects to see the update immediately on the next page load) is a poor fit for a purely eventual-consistency model unless you design around it deliberately, for instance by having the initiating request also update a local read model synchronously.

Handlers must be idempotent. Nearly every practical message broker offers "at-least-once" delivery, not "exactly-once." That's not a limitation of any particular product — it reflects the fundamental difficulty of guaranteeing exactly-once delivery across a network that can fail in the middle of an acknowledgment. In practice, this means your consumer will, sooner or later, receive the same event twice: because a consumer crashed after processing but before acknowledging, because of a broker rebalance, because of a retry after a transient network error. If handling PaymentCaptured twice means capturing a payment twice or sending a duplicate confirmation email, the system has a real defect. Idempotency has to be designed in — typically via a deduplication key, an upsert instead of an insert, or a check against already-processed event IDs — not treated as an edge case to patch later.

Debugging spans an asynchronous chain. When something goes wrong three services downstream from where it started, the practical question — where did this actually break, and why — is harder to answer than in a synchronous system, because there's no single thread of execution to step through. Effective event-driven systems typically compensate with strong structured logging tied to a correlation ID carried in every event's metadata, dashboards showing queue depth and consumer lag per topic, and dead-letter queues that capture failed messages instead of losing them silently. None of this is exotic, but it has to be built deliberately — it's not a byproduct of picking an event broker.

Three patterns, three very different complexity budgets

"Event-driven" is not one pattern — it's a spectrum, and the three points on it below have meaningfully different costs. Treating them as interchangeable is a common source of over-engineering.

PatternWhat it looks likeWhat it's good forComplexity cost
Event notificationA thin event says something happened (OrderPlaced, order ID only). Consumers that need details call back to the source service or a shared data store.Simple fan-out where consumers already have access to the data, or where minimizing payload size and coupling to a specific schema matters most.Low. Closest to a synchronous system's mental model; easiest to reason about and retrofit.
Event-carried state transferThe event carries enough of the relevant data that consumers don't need to call back at all (OrderPlaced includes line items, totals, customer ID).Decoupling consumers from the availability of the source service, reducing chatty callback traffic, letting consumers build their own local read models.Moderate. Payload schemas now need versioning discipline, and duplicated data across services can drift if not reconciled carefully.
Event sourcingThe sequence of events is the system of record. Current state isn't stored directly — it's derived by replaying events from the beginning (or from a snapshot).Domains where the history of changes matters as much as the current state — audit-heavy financial ledgers, workflow engines, systems that need to answer "what did this look like at time T."High. Requires event schema versioning as a first-class concern, snapshotting for performance, and a fundamentally different mental model for queries, since you can no longer just read a row.

Event notification and event-carried state transfer are both reasonable defaults for the kind of integration and fan-out scenarios most systems actually face, and the step from one to the other is incremental. Event sourcing is a different order of commitment. It solves real problems — a complete, tamper-evident audit trail, the ability to reconstruct historical state, natural support for temporal queries — but it also means every future change to what an event contains has to account for every previously stored event of that type, that replaying a long event stream to rebuild state has real performance implications that eventually require snapshotting, and that debugging "why does this record have this value" means reasoning through a chain of historical events rather than reading a single row. Event sourcing earns its complexity in domains where the audit trail or point-in-time reconstruction is the actual point of the system. It is a poor default for a typical CRUD-backed application, and adopting it because it appeared in an architecture blog post rather than because the domain specifically demands it is one of the more expensive mistakes an engineering team can make.

Message broker fundamentals worth understanding before you pick one

Regardless of which pattern you choose, the mechanics of the broker sitting between producers and consumers determine a lot of your system's actual behavior under load and failure.

Durable queues. A durable queue persists messages to disk (or a replicated log) rather than holding them only in memory, so a broker restart or crash doesn't silently drop events that were published but not yet consumed. This is what makes the "consumer can be temporarily unavailable" benefit real rather than theoretical — without durability, an event published while a consumer is down is simply lost.

Consumer groups. Most brokers let multiple instances of the same consumer service share the work of processing a stream, with the broker ensuring each message goes to only one instance within the group (while still being deliverable to other, independent groups subscribed to the same stream). This is what lets you scale consumers horizontally — running three instances of an order-processing service behind the same consumer group spreads load across them automatically — while still allowing a completely separate service, in its own consumer group, to receive every one of the same events for its own purposes.

Dead-letter queues. When a consumer repeatedly fails to process a specific message — a malformed payload, a bug triggered by a specific data shape, a downstream dependency that's permanently rejecting the request — retrying forever just wastes resources and can block the queue behind it. A dead-letter queue is where the broker (or the consumer's own retry logic) routes a message after it has failed a bounded number of times, so it stops blocking the healthy flow of other messages and becomes something a human or a separate remediation process can inspect. Any production event-driven system needs this mechanism designed in from the start; retrofitting it after a poison message has already caused an outage is a needlessly painful way to learn the lesson.

Ordering guarantees. Not all brokers guarantee that events are delivered in the order they were published, and even the ones that do (typically only within a single partition or shard) can lose that guarantee the moment you scale consumers horizontally within a group. If your domain has operations that must be processed in strict order — for instance, an AccountDebited event must never be processed after an AccountClosed event for the same account — you need to understand and explicitly design for your specific broker's ordering semantics rather than assuming events arrive in the sequence they were sent.

When events are genuinely the right fit

Event-driven architecture earns its complexity in a specific set of situations:

  • Integration between independently deployed services or teams. When two services are owned by different teams, deployed on different schedules, and shouldn't need to coordinate a release to add new interactions, events let one team publish facts and other teams build new consumers without requiring a change to the publisher.
  • Workflows that are naturally a sequence of state changes. Order fulfillment, document processing pipelines, and multi-step approval workflows are all naturally modeled as a chain of "this state change happened, trigger the next stage," which maps directly onto a sequence of events rather than a single service orchestrating every step synchronously.
  • Decoupling a slow downstream process from a fast upstream one. If accepting a user's request takes milliseconds but fully processing it (generating a report, transcoding a video, running a batch calculation) takes seconds or minutes, forcing the user-facing request to wait synchronously for the slow part is a poor experience and a fragile dependency. Publishing an event and processing it asynchronously lets the fast path stay fast.
  • One action needs to trigger several unrelated reactions. When a single business event legitimately needs to notify multiple, independently maintained systems — billing, analytics, notifications, a partner integration — a single publish reaches all of them without the publisher needing to know they exist.

When a plain synchronous call is the correct, boring answer

Events are not the default good choice, and a lot of production complexity traces back to teams reaching for them out of habit rather than fit. A synchronous request-response call remains the more appropriate choice when:

  • The operation is a standard CRUD action. Reading, creating, updating, or deleting a single resource with no downstream fan-out has no real coupling problem to solve, and a synchronous call is simpler to write, test, trace, and reason about.
  • The caller needs an immediate answer to show the user. Anything on the critical path of a user waiting for a response — form validation, a search query, checking whether a username is available — needs the answer now, not eventually. Making this asynchronous doesn't remove the need for a fast answer; it just adds a layer of polling or websocket plumbing on top to fake synchronicity, which is more complexity, not less.
  • Strong immediate consistency actually matters for correctness. If a user must never be shown stale data — an account balance immediately after a transfer, for example — eventual consistency introduces a real correctness risk that has to be specifically designed around rather than shrugged off.
  • There's exactly one consumer and the interaction is naturally a request for an outcome, not an announcement of a fact. If what you're modeling is fundamentally "do this thing and tell me if it worked," that's a command, and wrapping it in event terminology to feel modern doesn't change what it structurally is.

A practical path for introducing events incrementally

Systems that adopt event-driven architecture successfully tend to do it at the boundary, not from the ground up. A workable sequence looks like this:

  1. Start with the integration points that already hurt. Look for places where a synchronous call is coupling two services that are deployed independently, or where one action already needs to trigger multiple downstream effects through a growing list of direct calls. That's the boundary worth converting first — not an arbitrary internal interaction that's working fine as a direct call.
  2. Pick event notification or event-carried state transfer, not event sourcing, as the default. Reach for event sourcing only when you've identified a genuine, specific need for a full audit trail or point-in-time reconstruction as a domain requirement, not as an architectural preference.
  3. Design the dead-letter queue and idempotency strategy before writing the first consumer, not after the first poison message causes an incident. Decide upfront how a handler will detect and skip a duplicate delivery, and where failed messages will land for inspection.
  4. Instrument tracing from day one. Propagate a correlation ID through every event's metadata so a single business transaction can be reconstructed across services after the fact. Retrofitting this into an already-running system is far more painful than building it in from the first event type.
  5. Keep the rest of the system synchronous. Nothing about adopting events at one boundary obligates you to convert every other interaction. Most durable systems end up as a deliberate mix: synchronous calls for anything on the user-facing critical path, events at the specific integration and fan-out points where the coupling problem is real.
  6. Revisit the broker choice as scale demands it, not before. A simpler queue is usually the right starting point; a more sophisticated, log-based broker earns its operational overhead once replay, very high throughput, or long retention windows become actual requirements rather than anticipated ones.

The common thread across all of this is that event-driven architecture is a tool for a specific set of coupling and fan-out problems, not a general upgrade over synchronous APIs. Systems that introduce it deliberately, at the boundaries where it solves a real problem, tend to age well. Systems that adopt it as a wholesale architectural stance tend to spend years afterward building tooling to answer questions — "what happened to this request," "why did this handler run twice" — that a synchronous system would have answered for free.

For teams we work with, this usually means Node.js services publishing and consuming events, since it's already the runtime most of that same codebase is written in — the broker choice matters far more than the language producers and consumers happen to run.

Frequently asked questions

Is event-driven architecture the same thing as microservices?

No. Microservices describes how a system is decomposed into independently deployable units; event-driven architecture describes how those units communicate. You can build microservices that talk exclusively through synchronous REST calls, and you can build a handful of larger services that communicate through events. They are complementary but separate decisions, and conflating them leads teams to adopt events by default simply because they've adopted services.

Do I need Kafka to do event-driven architecture?

No. Kafka is one option among several — Redis Streams, RabbitMQ, Amazon SQS/SNS, and cloud-native equivalents can all carry events. Kafka's log-based retention and consumer-group model suit high-throughput, replay-heavy use cases, but it also brings meaningful operational overhead. A simpler queue is often the correct starting point, with a migration to something like Kafka reserved for when scale or replay requirements actually demand it.

How do I handle a downstream service that got an event but crashed before finishing its work?

This is exactly what at-least-once delivery guarantees are built around. The broker redelivers the event, either automatically after a visibility timeout or when the consumer group rebalances. Your handler needs to be idempotent so reprocessing the same event twice doesn't duplicate the side effect, and you need a dead-letter queue so an event that fails repeatedly is quarantined for inspection rather than retried forever.

What's the difference between event-carried state transfer and event sourcing?

Event-carried state transfer means an event includes enough data that consumers don't have to call back for details — it's still just a notification with a payload attached. Event sourcing is a more fundamental architectural commitment where the sequence of events is the system of record, and current state is derived by replaying them. The former is a moderate step up from plain notifications; the latter is a much larger commitment that changes how you model data, debug issues, and evolve schemas.

Can I mix synchronous APIs and event-driven communication in the same system?

Yes, and most production systems do exactly that. A typical pattern is synchronous APIs for anything a user is waiting on directly, with events used at integration boundaries between services, for fan-out notifications, and for decoupling slow background work from the request that triggered it. Very few real systems are purely one or the other.

Have a specific question about your project?

This article covers the general pattern — the fastest way to get a specific answer is to ask us directly.