Event-driven
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 22 min · 6 figures

    Event-driven architecture

    Building a system around events: events versus commands, the four things people mean by event-driven, pub/sub versus logs versus queues, what decoupling buys and costs, the dual-write trap, event design, and when not to use it.

    In an event-driven architecture, services announce facts such as “order 7781 was placed” to a broker, and any service that cares reacts in its own time. The producer does not know who is listening and does not wait for them. That buys loose coupling, independent scaling and new consumers without touching the producer; it costs immediate consistency, simple debugging, and an end-to-end flow you can read in one place.

    Picture it first: the station announcer

    A railway station does not phone each person when a train comes in. The announcer says once: “Train 12951 has arrived on platform 3.” Porters head over, the tea stall puts a kettle on, relatives walk to the platform. The announcer does not know who is listening, waits for no replies, and changes nothing when a new stall opens.

    Three details matter:

    • The announcement is a fact about the past. Nobody can reject “the train has arrived”. “Porter 14, go to platform 3” is different: an instruction to one person, who might refuse.
    • A loudspeaker has no memory. Miss it and it is gone, so the station also keeps an arrivals register that a latecomer can read back.
    • The ticket counter is different again. Ten clerks share one line, and each passenger is served by exactly one clerk.

    Map it back: the announcement is an event, the instruction a command, the announcer the producer, everyone who reacts a consumer, and the PA system the broker. The loudspeaker is pub/sub, the register a replayable durable log, and the ticket counter a work queue with competing consumers.

    Events, commands and queries

    An event states something that already happened: OrderPlaced, RiderAssigned. It is past tense and immutable: you cannot un-place an order, you publish OrderCancelled. Any number of consumers may care, and the producer does not know which.

    A command asks one receiver to do something: AssignRider, RefundPayment. It has one owner, it can be refused (“no riders nearby”), and the sender usually cares about the outcome.

    A query asks for data and changes nothing: “what is order 7781’s status?” The caller waits for the answer.

    Event Command Query
    Receivers Any number, unknown to the sender One handler One handler
    Can it be refused? No; it already happened Yes Yes
    Sender waits? No Often, for the outcome Yes

    A producer emits events, a consumer reacts to them, and between them sits an event channel, usually a topic, run by a broker or router such as Kafka, Amazon EventBridge or Azure Event Hubs.

    The passive-aggressive command

    Martin Fowler’s name for an event that is really an order in disguise. If payments publishes RefundRequired and breaks when the refunds service ignores it, that was a command with the owner hidden. Send a command when a specific party must act and you care whether it did; publish an event when you are stating what happened.

    What events buy, and what they cost

    Take checkout in a Swiggy- or Zomato-style food delivery app. When a customer pays, five things follow: the kitchen tablet rings, rider assignment starts searching, the customer gets a push and an SMS, loyalty points are credited, and analytics counts the order.

    The request-driven way is for the orders service to call all five. Checkout becomes as slow as the slowest call and as fragile as all five together: if each is up 99.9% of the time and any failure fails checkout, it succeeds 0.999^5 ≈ 99.5% of the time. Over a 30-day month (43,200 minutes) that is about 216 minutes of failure, 3.6 hours, instead of 43 minutes. And adding a sixth reaction, say an ETA model, means changing and redeploying checkout.

    The event-driven way: orders saves the order and appends one OrderPlaced event to a topic, and each service subscribes and reacts in its own time. Checkout waits for one durable write, not five services.

    Checkout in a food delivery app, before and after. On the left the orders service calls five downstream services itself; on the right it appends one OrderPlaced event and five consumers read it.Notice The dashed ETA box: on the left it needs a change to Orders, on the right it just subscribes.

    Giving up control of what happens next, in return for not depending on it, is the whole style. See also synchronous vs asynchronous communication.

    What you gain

    • Temporal decoupling. Producer and consumer need not be up together: if rider assignment is down, checkout still takes orders and events wait.
    • Independent scaling. Each consumer scales to its own load, up to one instance per partition.
    • New consumers without touching the producer. An ETA model just subscribes; with a retained log it can start from last week’s events.
    • Burst absorption. In a Big Billion Days-style sale, producers append fast and consumers drain steadily (see backpressure and load shedding).
    • Audit and replay. A retained log records what happened, and a fixed bug can be re-run over history.

    What you pay

    • Eventual consistency. For milliseconds normally, minutes in an outage, services disagree, so screens must show in-between states (see strong vs eventual consistency).
    • Duplicates. Delivery is at least once: a consumer that crashes after acting but before recording progress sees it again, so consumers must be idempotent (see delivery guarantees and DLQs).
    • Ordering only per key. OrderCancelled arrives after OrderPlaced only if both share a key, and so a partition (see message ordering).
    • Harder debugging. No call stack spans the flow; you need correlation IDs, traces through the broker and consumer-lag dashboards (see observability).
    • Schema evolution. Producers and consumers deploy separately, so an event’s shape is a public contract.
    • Event spaghetti. With dozens of services reacting to each other, nobody can draw the end-to-end flow; Fowler warns it often becomes visible only by watching the live system.
    • Poison messages. One event that always crashes its consumer can stall everything behind it.

    In the interview

    Name the price with the benefit: “Publishing OrderPlaced decouples checkout from five services, so it stays up when they are down. The cost is eventual consistency, so the app shows ‘finding a rider’ until RiderAssigned arrives, and duplicates, so every consumer dedupes on the event ID.”

    Four things people mean by “event-driven”

    In 2017 Martin Fowler wrote up a Thoughtworks summit which found that “event-driven” meant at least four different patterns, and that teams blamed the wrong one when a project hurt. Each has its own bill.

    1. Event notification. The producer announces a change and does not care what happens next. The event can be thin: an order ID and a link, and a consumer that needs details calls back. You gain very low coupling and a simple setup. You pay because callbacks keep the producer’s load and availability on the consumers’ path, and a flow spread across several notifications appears in no single program.

    2. Event-carried state transfer. The event carries everything consumers need, and they keep their own copy instead of calling back. You gain resilience (loyalty works while orders is down), lower latency and no read load on the source. You pay with bigger events, many copies of the same data, copies that lag, and consumers that must maintain that state correctly.

    3. Event sourcing. The events are the source of truth: every change is stored as an event and current state is derived by replaying them, the way Git derives your working tree from commits. You gain a full audit trail, state as of any past moment, and new views from old history. You pay with old events that must stay readable forever, replays that must not re-send SMSes, and snapshots to keep rebuilds fast.

    4. CQRS. Command Query Responsibility Segregation separates the model you write to from the models you read from. Strictly it is not about events, but events often feed the read side. You gain read models tuned to each screen; you pay with two models to keep in step and reads that lag writes. Fowler notes many colleagues are wary of it because it is so often misused.

    Fowler’s four meanings of “event-driven”, drawn on the same order. Each panel shows what travels, where the truth lives and what it costs.Notice Only panel 1 sends a request back to Orders, and only panel 3 makes the log itself the source of truth.

    Thin or fat events?

    This is the everyday choice between patterns 1 and 2. Thin events keep one system of record and small contracts, but consumers call back, re-coupling at runtime. Fat events let consumers work alone, but every field becomes a promise and copies drift. A sensible middle: carry the stable fields most consumers need (order ID, restaurant, items, total), and leave out phone numbers and addresses, since every subscriber sees every event. More in event sourcing and CQRS.

    Request-driven Event notification Event-carried state transfer Event sourcing
    What travels A call and its reply A thin event: IDs A fat event with the data consumers need Every state change, kept as the record
    Runtime coupling High: the callee must be up Low, but consumers call back Lowest: consumers work alone Low for readers of the log
    Contract coupling API signature Small: type and IDs Large: every field is a contract Largest: old events stay readable forever
    Consistency Immediate for that call Eventual; callbacks read fresh data Eventual; copies lag Strong in the writer; projections eventual
    Replay None Only from a retained log Rebuild copies from a retained log Native: state is a replay
    Complexity Lowest Low Medium: copies to maintain High: snapshots, upgrades, projections
    Typical use Login, balance checks “Something changed, go look” Catalogue or profile copies Ledgers, audit-heavy domains
    Request-driven calls against the three event styles. Each row is the typical case; retention, payload design and tooling can move it.

    Three ways to move events

    Pub/sub routers

    A router keeps a list of subscriptions and pushes each event to every match. In Amazon EventBridge, a rule matches events by pattern on source, type or content and delivers them to targets: up to five per rule, with 300 rules per bus by default. Filtering lives in the broker, so each consumer receives only what it asked for.

    The catch is memory. In plain pub/sub an event goes to the current subscribers and is gone: a subscriber added tomorrow never sees it, and one that is down relies on the router’s retries. The fixes are a durable queue per subscriber (SNS fanning out to SQS queues is the classic AWS shape) or a replayable archive, which EventBridge offers. More in publish-subscribe.

    Event streaming on a durable log

    Kafka and Azure Event Hubs keep events in an append-only log split into partitions. The producer picks a partition by hashing the event’s key, so every event for order 7781 lands in one partition, and each partition is read in exactly the order it was written. Across partitions there is no order.

    Consumers pull. Within a consumer group, each partition is read by one member at a time, so the group’s position in it is one number: the offset of the next event. Groups keep separate offsets, so kitchen can be at the head while analytics runs an hour behind. Reading deletes nothing; events stay for the topic’s retention period, so a new consumer can start from the oldest, and a buggy one can rewind and reprocess after the fix.

    A Kafka-style topic with three partitions keyed by order ID. Two consumer groups read the same events, each from its own offsets.Notice The rider group is 18 events behind and the kitchen group is not slowed by it at all: lag belongs to a group, not to the topic.

    Partitions also cap parallelism: extra group members beyond the partition count sit idle. Choose the count with growth in mind, because adding partitions later changes which partition each key hashes to, and Kafka cannot reduce the count. Recent Kafka versions add share groups, queue-style consumption where members can outnumber partitions and records are acknowledged one by one. See Kafka.

    Point-to-point queues

    A work queue hands each message to one of several competing consumers, which acknowledge it when done. That suits distributing work (“send this SMS”) and is how commands usually travel. It is not a broadcast: a second service reading the same queue steals messages instead of getting copies. See message queues.

    Pub/sub router Durable log Work queue
    Delivery A copy to every matching subscriber Every group reads every event Each message to one consumer
    After consumption Gone, unless archived or queued Kept until retention expires Deleted on acknowledgement
    Late joiners, replay No, or from an archive Yes: rewind the offset No
    Ordering None by default Per partition, so per key None by default
    Typical use Routing many event types to many targets High-volume domain events, analytics, CDC Jobs and commands

    Who owns the flow: choreography or orchestration

    Azure calls the two ways to run a multi-step process over events the broker and mediator topologies; you will more often hear choreography and orchestration.

    Choreography (broker topology). Nobody is in charge. Orders publishes OrderPlaced; the restaurant accepts and publishes OrderAccepted; rider dispatch reacts with RiderAssigned; notifications react to all three. You gain maximum decoupling and easy extension. You pay with a flow that lives nowhere: “why is order 7781 stuck?” means checking several services’ logs, and nobody owns retries or compensation when step 3 fails.

    Orchestration (mediator topology). An orchestrator, often a workflow engine, holds each order’s state, sends commands (accept, assign, notify) and owns timeouts, retries and compensation. You gain visibility and explicit error handling; you pay with coupling to the orchestrator, which must scale and stay up.

    The same order flow run two ways. In choreography each service reacts to an event and emits the next one; in orchestration one workflow sends commands and tracks each order’s step.Notice The orchestrated side has one box that knows where order 7781 is. The choreographed side has to ask everyone.

    A good default: choreograph side reactions such as notifications, analytics and loyalty; orchestrate the core flow with real steps, deadlines and compensations. More in multi-step workflows, workflow engines and 2PC vs sagas.

    The dual-write trap

    The classic bug in a new event-driven system is two writes with no transaction around them. Orders commits the order, then publishes OrderPlaced. Crash in between and the order exists but no kitchen hears of it. Publish first and you get the reverse: an event for an order whose write then fails. No transaction spans your database and your broker, and two-phase commit across them is rarely available or worth it.

    The transactional outbox fixes it. In the same local transaction as the order row, insert the event into an outbox table. A relay then publishes new outbox rows, either by polling the table or by tailing the database’s log with change data capture. The event is published if and only if the order commits, in commit order. The relay can crash after publishing but before marking a row sent, so it may publish twice: one more reason consumers dedupe.

    Left: the order is committed, then the process crashes before publishing, so the event is lost. Right: the order row and the outbox row commit together, and a relay publishes from the outbox.Notice The outbox turns two writes to two systems into one local transaction, and pushes the remaining risk (a duplicate) onto consumers, who must handle duplicates anyway.

    The full treatment is in transactional outbox.

    Designing events

    Name them after business facts

    Use past-tense domain language a product manager would recognise: OrderPlaced, RiderAssigned. Avoid database-shaped events such as OrdersRowUpdated, which leak your schema, and commands in disguise such as SendWelcomeSms. Distinct types also let routers filter without parsing payloads.

    Wrap them in an envelope

    Keep metadata apart from data, so routers, logs and dedupe logic work without parsing the payload. CloudEvents is a vendor-neutral standard for that envelope, with four required attributes:

    • id: unique within its source. Consumers may treat a repeated source plus ID as a duplicate, so this is your dedupe key.
    • source: where it happened, as a URI reference.
    • type: what happened, ideally prefixed with a reverse-DNS name.
    • specversion: 1.0.

    Optional attributes include time (RFC 3339), subject (the thing it concerns), dataschema and datacontenttype, and you may add extensions such as a correlation ID.

    {
      "specversion": "1.0",
      "id": "9f3c2a1e-6d2b-4b7e-9a51-2f0c8d7e4b10",
      "source": "/orders-service/ap-south-1",
      "type": "in.example.food.order.placed.v1",
      "subject": "order/7781",
      "time": "2026-10-27T19:42:05.120Z",
      "datacontenttype": "application/json",
      "correlationid": "chk-55b1e0",
      "data": {
        "orderId": "7781",
        "restaurantId": "r-204",
        "items": [{ "sku": "paneer-tikka", "qty": 2 }],
        "totalPaise": 64800,
        "city": "BLR"
      }
    }

    Choose the partition key on purpose

    On a log, the key decides both ordering and load spread. Key by order ID and each order’s events stay in sequence while load spreads evenly. Key by restaurant ID and a famous biryani place at 8 pm becomes a hot partition one consumer must handle alone; key by city and Bengaluru swamps its partition. Pick the smallest entity whose events must stay in order, and check that no single key carries a big slice of the load.

    Version the schema, not just the code

    Treat each event type as a published API. Adding an optional field with a default is safe; removing, renaming or retyping one breaks someone. A schema registry enforces this at publish time: Confluent’s checks each new version against the previous one, and in its default BACKWARD mode consumers on the new schema can read old events, so you upgrade consumers first. For a breaking change, publish a new type or topic (order.placed.v2), run both while consumers move, then retire the old one. See serialization and schema evolution.

    Add the correlation ID on day one

    Retrofitting tracing into a decoupled system is far harder than starting with it. Put the checkout request’s correlation ID in every event, and have each consumer copy it into the events it emits and its logs, so one search shows an order’s whole journey.

    Worked example: one order, five consumers

    Assume an illustrative dinner peak of 500 orders a second, so OrderPlaced arrives at 500 events/s. The topic order-events has 12 partitions keyed by order ID, read by five consumer groups: kitchen, rider assignment, notifications, loyalty and analytics.

    Adding a consumer. The data science team deploys a new group, eta-model, subscribed to the topic. Nothing changes in orders or the other groups, and the model can start at the latest offset or backfill from the oldest retained one. Request-driven, orders would add a sixth call and redeploy checkout.

    Rider assignment goes down for 10 minutes. A bad deploy crashes it at 8:00 pm. Checkout and the other four groups carry on; only the rider group stops, so its backlog grows by 500 events every second:

    500 events/s × 600 s = 300,000 events waiting at 8:10 pm.

    The deploy is rolled back at 8:10. Rider assignment runs 4 instances that together process 2,000 events/s, but new orders keep arriving at 500/s, so the backlog shrinks by only:

    2,000 − 500 = 1,500 events/s net, and 300,000 ÷ 1,500 = 200 s to drain.

    The group catches up at 8:13:20. How late was each order? An event arriving t seconds into the outage sits behind 500 × t earlier events, so it is handled at 600 + 500t ÷ 2,000 = 600 + t/4 seconds. The first order of the outage waited the full 10 minutes; the one placed at recovery waited 750 − 600 = 150 s.

    The rider group’s backlog during and after a 10-minute outage, with orders arriving at 500 events/s. The solid green line drains at 2,000 events/s; the dashed line at 1,000 events/s.Notice With only twice the arrival rate as capacity, recovery takes as long as the outage itself.

    Three lessons:

    • Headroom sets recovery time. With 1,000 events/s of capacity, twice the arrival rate, the net drain is 500/s and catching up takes 600 s, as long as the outage. The Amazon Builders’ Library gives the same rule: recovering from an hour-long outage takes double capacity for another hour.
    • Partitions cap scale-out. With 12 partitions the rider group can use at most 12 active instances. At the same 500 events/s per instance, that is 6,000/s, draining 300,000 events at 5,500/s net in about 55 s. Beyond that you need more partitions, and adding them later reshuffles keys.
    • Choose the drain order per consumer. For rider assignment the oldest orders are the most urgent, so first-in, first-out is right. A 10-minute-old “order accepted” push is noise, so notifications should drop or sideline stale events and serve fresh ones first, as the Builders’ Library describes. Alert on the age of the oldest unprocessed event, not just the count.

    When events are the wrong tool

    • The caller needs an answer now. Login, a balance check, a UPI payment authorisation. Request-reply over a pair of queues works, but adds hops and a correlation step to get what one HTTP call gives you (see REST, GraphQL and gRPC).
    • Simple CRUD. A Zoho- or Freshworks-style B2B settings screen with one database and one team gains only another system to run.
    • Strong invariants across entities. “Never sell the same berth twice”, “a wallet never goes negative”. Enforce them in one service with one transaction, and announce the result afterwards (see contention and double booking).
    • A team new to asynchronous systems. Debugging, monitoring and recovery are different skills, as Azure’s guidance warns.

    Most real systems are hybrids: synchronous calls on the user’s critical path, events for whatever can happen a moment later. Events work inside a modular monolith as well as between microservices, cross company boundaries as webhooks, and trigger most serverless functions.

    Going deeper: failure modes

    Poison messages and dead-letter queues

    On a log, a consumer that cannot process an event cannot move its offset past it, so one malformed event blocks its partition. Uber’s insurance engineering team described the standard fix: after a few failed attempts, publish the event to a retry topic and commit the offset, so live traffic keeps flowing. Each retry level waits longer, and the last stop is a dead-letter topic engineers can inspect, purge or merge back. Errors are routed by type: network blips go to retries, while code bugs go straight to the DLQ, since retrying cannot fix them. The costs: retried events are processed out of order, and an unwatched DLQ is just slow data loss, so alert on it.

    Backlogs are bimodal

    The Builders’ Library describes two modes: fast with no backlog, and ever slower once arrivals outpace processing. In its example, a spike at 10× what consumers are scaled for runs for the 30 minutes it takes an operator to step in, and working through it takes 300 minutes. Keep headroom, isolate noisy workloads in separate topics, watch message age, and push back on producers when a backlog keeps growing. And mind retention: if a consumer stays broken longer than the topic keeps events, what it missed is gone and must be backfilled from the source of truth.

    Replays and loops have side effects

    Replaying a log also re-runs whatever the consumer does: replay a week of OrderPlaced through notifications and customers get a week of old SMSes. Keep state updates, which are safe to replay, apart from external side effects, which need idempotency keys or a replay mode that skips them. Choreography can also loop, a price change triggering a re-index whose update triggers a price rule; carry a causation ID (the ID of the event that caused this one) to detect cycles, and keep an event catalogue of who produces and consumes each type.

    Cheat sheet

    Idea Remember it as
    Event vs command Past-tense fact for anyone vs request to one handler that can refuse
    Fowler’s four Notification, event-carried state transfer, event sourcing, CQRS
    Thin vs fat Thin: IDs, consumers call back. Fat: consumers keep copies
    Pub/sub router Pushes copies to matching subscribers; no replay unless archived
    Durable log Key-hashed partitions, order per key, offsets per group, rewind and replay
    Work queue Competing consumers; one consumer per message; for jobs and commands
    Choreography vs orchestration Everyone reacts vs one owner sends commands and tracks state
    Dual write Transactional outbox plus a relay (polling or CDC)
    Duplicates At-least-once delivery, so dedupe on source + event ID
    Envelope CloudEvents: id, source, type, specversion, plus time and subject
    Backlog maths Backlog = arrival rate × outage; drain = backlog ÷ (capacity − arrival rate)
    Don’t use for Answers needed now, simple CRUD, cross-entity invariants

    How to say it in the interview

    “Checkout writes the order and an OrderPlaced event in one transaction through an outbox, and a relay publishes it to a Kafka topic keyed by order ID, so each order’s events stay in order. Kitchen, rider assignment, notifications, loyalty and analytics are separate consumer groups, so a slow one only builds its own lag. Delivery is at least once, so consumers dedupe on the event ID, and a schema registry keeps changes backward compatible. Payment and anything else the user waits on stays synchronous. I’d orchestrate the restaurant-and-rider flow so one place knows each order’s step, and choreograph the side effects.”

    What happens if a consumer is down for 10 minutes? Its lag grows at the arrival rate: 500 events/s × 600 s = 300,000 events, and nothing else notices. On restart it drains at capacity minus arrival rate, 1,500/s net, so in 200 s. Size capacity with headroom and alert on the age of the oldest event.

    How do you keep events in order? Only per key. Partition by the entity whose events must stay ordered, usually the order ID; one consumer per partition per group handles them in sequence. Never rely on global order, and carry a version number so consumers can ignore stale updates.

    How do you avoid processing an event twice? You make duplicates harmless. Record processed event IDs in the same transaction as the effect (an inbox table), or make the effect idempotent: “set points for order 7781 to 50”, not “add 50 points”. See idempotent consumers.

    When would you not use events? When the caller needs the answer to continue (login, balance, payment authorisation), for simple CRUD owned by one team, and for invariants across entities that need one transaction.

    Where to go next

    References and further reading

    1. What do you mean by “Event-Driven”? (Martin Fowler, 2017)martinfowler.comThe four patterns behind the word: event notification, event-carried state transfer, event sourcing and CQRS, with the gains and traps of each, including the passive-aggressive command.
    2. Event-driven architecture style (Azure Architecture Center)learn.microsoft.comPub/sub versus event streaming, broker versus mediator topology, thin versus full payloads, when not to use the style, correlation IDs and schema evolution.
    3. CloudEvents specification v1.0github.comThe required context attributes (id, source, specversion, type), the optional ones (time, subject, dataschema, datacontenttype), and source plus id as the duplicate-detection key.
    4. Apache Kafka documentation: Designkafka.apache.orgKey-hashed partitions, one consumer per partition per group, the offset as a consumer’s position, rewinding to reprocess, at-least-once delivery and share groups.
    5. Apache Kafka documentation: Basic operations (modifying topics)kafka.apache.orgAdding partitions changes which partition a key hashes to, and the partition count cannot be reduced.
    6. Amazon EventBridge: event bus conceptsdocs.aws.amazon.comA router with rules that match event patterns, up to five targets per rule, 300 rules per bus by default, and archive and replay.
    7. Avoiding insurmountable queue backlogs (Amazon Builders’ Library)aws.amazon.comBimodal queue behaviour, the double-capacity recovery rule, measuring message age, sidelining or dropping old messages, and propagating request IDs.
    8. Pattern: Transactional outbox (microservices.io)microservices.ioWhy a database write plus a broker publish needs an outbox, the message relay, ordering, and why the relay can publish duplicates.
    9. Building Reliable Reprocessing and Dead Letter Queues with Apache Kafka (Uber Engineering, 2018)uber.comHow failed events block a partition, a ladder of delayed retry topics ending in a DLQ, and routing errors by type.
    10. Schema evolution and compatibility (Confluent Schema Registry)docs.confluent.ioBackward, forward and full compatibility, BACKWARD as the default, which side to upgrade first, and a new topic for breaking changes.