Event-driven architecture: patterns, brokers, and trade-offs
Event-driven architecture means services announce what happened and let whatever's listening react independently. How publish and subscribe changes coupling, the core patterns, how brokers differ, and the eventual consistency cost teams underestimate.
On this page
Event-driven architecture is a way of structuring software so that services announce facts instead of calling each other to request an action: a payment cleared, a file finished processing, a sensor crossed a threshold. Whatever depends on one of those facts subscribes to it and decides independently what to do next, and the service that raised the event doesn't know who's listening or how many.
That indirection is the payoff for the added complexity. A subscription cancellation is one event: billing stops the next charge, a retention workflow queues an offer email, and an analytics job logs the churn reason, three services reacting to one fact without calling each other.
Publish, subscribe, and coupling
Publish and subscribe names the two roles. A publisher emits an event to a channel, a topic or a queue depending on the broker, without specifying who should act on it. A subscriber registers interest in that channel and gets every event published there, whether one service is listening or twenty, and neither side holds a reference to the other.
That's the real shift from a direct call: change the callee's interface and the caller breaks immediately, and the caller has to know the callee exists and wait for a response. An event carries none of that weight; consumers can be added, removed, or rewritten without touching the publisher's code. System integration treats this as one pattern among several; event-driven architecture is a system built around that shape by default, not bolted on for a single connection.
Event-driven architecture patterns
Event notification is the plainest version: the event carries just enough to identify what happened, an order ID and a status, say, and a consumer that needs more detail calls back to fetch it.
Event-carried state transfer skips that callback by putting the full record in the event instead of a pointer to it. Consumers stay independent even when the source system is down, but each now holds a copy that can drift out of date if never refreshed.
Event sourcing makes the event log itself the source of truth rather than a side effect of one: instead of storing current state and overwriting it, the system stores every event and derives state by replaying them. That gives a full audit trail but costs a system that's harder to query directly.
CQRS splits the model in two: one path handles writes, a separate model serves reads. It's frequently paired with event sourcing: the write side appends events, the read side builds projections from them, though the two are separable. CQRS costs a read model that lags the write model by however long its projection takes to catch up, the same eventual consistency trade the next section covers in full.
Choosing a broker
Picking a broker means picking a set of constraints a product name doesn't capture.
Kafka is a distributed log: every event lands in a partition, ordered within it, and stays for a configurable retention window instead of disappearing once read. That lets multiple consumer groups replay the same stream independently. Running it well means operating a cluster, so it pays off at real volume, not for a handful of events an hour.
RabbitMQ is a message broker built around queues and routing rather than a durable log: an exchange routes each message to a queue, direct, topic, or fanout, and it's gone once a consumer acknowledges it. That fits work distribution and complex routing well; replaying history isn't an option, since none is kept.
Cloud-native services, AWS's SNS/SQS, Google Cloud Pub/Sub, trade that control for a much smaller operational footprint: a managed topic paired with a managed queue covers the fan-out-then-process shape most teams need, though it gives up the replay depth a self-run platform offers.
The cost of eventual consistency
Every pattern above trades one thing for decoupling: the guarantee that anyone reading data right now sees its latest version. A consumer processes an event on its own schedule, anywhere from milliseconds to minutes behind, so two parts of the same system can disagree about one fact for a real stretch of time.
Delivery guarantees add to this rather than fix it. Brokers overwhelmingly promise at-least-once delivery: a dropped connection or a crash before acknowledgment redelivers a message, so the same event can be processed twice. Treating exactly-once as the default is a mistake that gets expensive fast; most systems need a consumer built to handle a repeat without double-counting anything, the idempotency question API integration already covers, applied here to a subscriber.
Ordering has the same shape: events from different producers, or different partitions of the same one, can arrive out of sequence. An operation that has to look atomic to a customer, an order and its payment, say, reaches for a saga instead of one transaction: a chain of local commits, each paired with a way to compensate if a later step fails, trading a brief partial state for never locking across services.
The case against going event-driven
A team running one service has nobody to publish to: every event it emits has exactly one consumer, itself. Publish and subscribe still adds a broker to operate, and turns a debugging session that used to be a stack trace into a search across several services' logs. A direct function call already did all of that for free.
The case gets real once independent teams need to react to the same facts without waiting on each other's release schedule: a logistics platform where pricing, notifications, and fraud checks are owned by three separate teams, each reacting to a shipment event independently. It also holds once one slow consumer, a compliance export, say, shouldn't block everything else.
A wrong schema is costlier here than a wrong call almost anywhere else in the stack, because it gets copied into every consumer that reads it, and fixing it later means coordinating a change across every subscribed team. That's what software development consulting is built to catch before the first schema ships; custom software development and product development carry out the buildout once it's settled. The debugging cost doesn't depend on team size; the benefit does, once more than one independent consumer is waiting on the other end.
Frequently asked questions
A way of building software where services announce what happened instead of asking each other to do something. A shipping service says a package left the warehouse; it doesn't know or care which other services act on that fact, or how many there are. Each one decides independently what to do next.
No, and most systems run both. A REST call looks something up right now; an event announces a fact for whatever's listening to react to later. Many systems expose synchronous APIs for lookups and publish events for state changes.
No, and neither one requires the other. Microservices describes how a system splits into independently deployable services. Event-driven architecture describes how those services, or a single monolith's internal modules, communicate once split. A microservices system can talk entirely through direct API calls, and a monolith can publish events internally without being broken into separate services at all.
Mainly through correlation IDs, one identifier assigned when an event is first published and carried through every service that processes something derived from it. A single query pulls every log line tagged with that identifier, instead of guessing which service to check first.
Keep exploring
System integration: patterns, methods, and when you need it
A plain-language reference on system integration, covering what it means, the four patterns teams actually use, how to pick one, and the failure modes that make integration projects overrun.
6 min readEngineeringTransaction management: ACID, isolation, and distributed systems
A short guide to transaction management - the ACID properties, what each SQL isolation level actually prevents, and why transactions across microservices need sagas rather than a database guarantee.
5 min readEngineeringAPI integration: how it works and what makes it hard
How API integration works: what it connects, the request and event styles behind it, how authentication and rate limits shape the design, and why a retry without an idempotency key is where it quietly stops being reliable.
6 min read