Event-Driven Architecture Concepts

Why this matters

Every reliability decision you'll make in this track — dead-lettering, the outbox pattern, idempotent consumers, schema versioning — is a consequence of a few core ideas: that a message can arrive twice or never, that the sender and receiver run at different times, and that "the event happened" is a different kind of statement than "do this thing." Get these concepts wrong and you'll build a system that works in a demo and double-charges a customer in production. This module is deliberately about vocabulary and trade-offs before you touch a single Azure resource — because the resources only make sense once the model does.

Concepts

Commands vs. events

A command is a request to do something — ChargePayment, ReserveInventory. It's imperative, addressed to one specific handler, and the sender expects it to be carried out. An event is a statement that something already happened — PaymentCharged, OrderPlaced. It's a fact in the past tense, the publisher doesn't know or care who consumes it, and it can have zero, one, or many interested consumers.

This distinction drives everything downstream. A command has one owner (who processes it); an event has one owner (who emitted the fact) but any number of reactors. When you designed Dapr service invocation in track 6 module 05 — app A calling POST /invoke/backend/method/charge — that was a command: synchronous, addressed, expecting a result. When you published to a Dapr pub/sub topic, that was an event: fire-and-forget, addressed to a topic rather than a handler. Naming events in the past tense (OrderShipped, not ShipOrder) is the single cheapest habit for keeping the two straight.

Delivery guarantees: at-most-once, at-least-once, exactly-once

When a message crosses a network and a broker, three delivery guarantees are possible, and you must pick one consciously:

  • At-most-once: the message is delivered zero or one times. If a consumer crashes before finishing, the message is lost. Cheap and simple; only acceptable when losing a message is fine (e.g. a "user is typing" signal).
  • At-least-once: the message is delivered one or more times. The broker redelivers until the consumer explicitly acknowledges success, so a crash mid-processing means a duplicate later. This is the default and the realistic one — Service Bus, Event Grid, and Dapr pub/sub all give you at-least-once.
  • Exactly-once: delivered and processed once, no loss and no duplicates. This is the one everyone wants and almost no distributed system truly provides end-to-end — it requires the broker and your side effect to participate in one atomic decision. In practice you synthesize it from at-least-once delivery plus an idempotent consumer (next section). The honest phrase is "effectively-once."

The practical takeaway: assume at-least-once, and design so a duplicate is harmless. Every module after this one is, in some sense, about surviving at-least-once delivery.

Idempotency: how you survive duplicates

An operation is idempotent if doing it twice has the same effect as doing it once. SET balance = 100 is idempotent; balance = balance + 100 is not. Because you're getting at-least-once delivery, your consumer will occasionally see the same event twice — a redelivery after a crash, a broker retry, a publisher that sent twice. If processing that event isn't idempotent, the duplicate becomes a double charge, a double email, a doubled inventory decrement.

The standard technique is a dedup key: every event carries a stable unique id (a messageId or a business key like an order id), and the consumer records which ids it has already processed — often by writing the id into the same database transaction as the side effect, then checking it on the way in. Seen it before? Acknowledge and skip. You'll implement exactly this in module 05, and the capstone requires you to write up an idempotent consumer design. Idempotency is the bridge that turns unavoidable at-least-once delivery into effectively-once behavior.

Coupling: temporal, and why async trades consistency for resilience

A synchronous call (track 6's service invocation) is temporally coupled: the caller and callee must both be up at the same instant, and the caller blocks and fails if the callee is down. An event on a durable queue is temporally decoupled: the publisher writes the event and moves on; the consumer processes it whenever it's ready — a second later or after a ten-minute outage. The broker's durability is what buys that decoupling.

That decoupling is the whole value proposition, and it comes at a price: you give up immediate consistency. Right after OrderPlaced is published, the inventory service hasn't decremented stock yet — the system is eventually consistent, not instantly. In exchange you get resilience (a consumer outage doesn't take down the publisher), independent scaling (a slow consumer builds a queue backlog instead of slowing the producer), and load leveling (a spike is absorbed by the queue). Event-driven architecture is this trade — eventual consistency for resilience and scale — made on purpose. If a workflow genuinely needs an immediate, consistent answer (is this card valid right now?), that's a command / synchronous call, not an event.

Point-to-point vs. publish/subscribe

Two delivery topologies sit under everything in this track, and they're taught in full — fan-out vs. competing consumers, and why a fire-and-forget bus and a durable queue make opposite promises — in Pub/Sub Architecture. In one line each: point-to-point sends each message to exactly one consumer, even when several compete for work off the same queue (how you scale a worker pool); publish/subscribe delivers every message to every interested subscriber, each with its own copy. The Dapr publish you did in track 6 was pub/sub; a classic work queue is point-to-point. Module 01 maps these two topologies onto the concrete Azure services (Service Bus queues vs. Service Bus topics vs. Event Hubs streams) — it's the axis the next module is organized around.

Command reference

This module is conceptual, so the "commands" here are the small set of az calls you use to look at the messaging services before diving in, plus the vocabulary mapping you'll reference all track.

Command What it does Example
az servicebus namespace list Lists Service Bus namespaces in your subscription (to confirm none are lingering) az servicebus namespace list -o table
az eventgrid topic list Lists Event Grid custom topics az eventgrid topic list -o table
az provider show Confirms a resource provider (e.g. Microsoft.ServiceBus) is registered on your subscription az provider show -n Microsoft.ServiceBus --query registrationState -o tsv
az provider register Registers a provider so you can create its resources az provider register -n Microsoft.EventGrid

Concept-to-service mapping you'll use for the rest of the track:

Concept Realized by (Azure) Delivery topology
Command / work queue Service Bus queue point-to-point (competing consumers)
Event fan-out Service Bus topic + subscriptions, or Event Grid publish/subscribe
Ordered event log / replay Event Hubs (Kafka-style stream) log, many independent readers
At-least-once with ack Service Bus, Event Grid, Dapr pub/sub all at-least-once

Flag breakdown — az provider show -n Microsoft.ServiceBus --query registrationState -o tsv:

  • -n Microsoft.ServiceBus — the resource provider namespace; every Azure service's resources live under one, and it must be Registered before you can create resources of that type.
  • --query registrationState — a JMESPath expression pulling just the one field you care about out of the JSON, so you get Registered instead of a page of output.
  • -o tsv — tab-separated output, i.e. the bare value with no quotes or formatting, ideal for scripting and quick checks.

Hands-on exercises

These are mostly pencil-and-paper / CLI-inspection exercises — the point is to cement the model before you provision anything. No billable resources are created here.

  1. Classify ten messages. Write down these ten and label each command or event, then rewrite any command-shaped name that's really an event into past tense: SendWelcomeEmail, UserRegistered, ChargeCard, PaymentDeclined, GenerateInvoice, InvoiceGenerated, ShipOrder, OrderShipped, RecalculatePricing, CartAbandoned. Verify your rule: events are facts already true; commands are requests not yet carried out.

  2. Pick the delivery guarantee. For each scenario, decide at-most-once, at-least-once, or effectively-once (at-least-once + idempotency) and justify it in one sentence: (a) a "typing…" indicator, (b) a payment capture, (c) a telemetry heartbeat sampled once a second, (d) sending an order-confirmation email, (e) decrementing inventory. Verify: anything with a real-world side effect that must not double should land on effectively-once.

  3. Diagnose and fix: a duplicate side-effect from a non-idempotent consumer (on paper). A billing consumer runs UPDATE accounts SET balance = balance - :amount WHERE id = :id on every FundsWithdrawn event, and support reports a customer was debited twice for one withdrawal. Diagnose: the broker delivers at-least-once, a redelivery (after a crash/retry) re-ran a non-idempotent subtraction, so one logical event applied twice. Fix: write down two concrete redesigns that make a duplicate delivery harmless — verify at least one uses a dedup key (the event's id recorded and checked before applying) and the other reframes the operation to be naturally idempotent (e.g. applying an absolute target balance rather than a relative delta). This is the exact bug you'll reproduce against a live broker in module 04.

  4. Draw the coupling. On paper, sketch OrderService → PaymentService as (a) a synchronous command call and (b) an event on a durable queue. For each, answer: if PaymentService is down for 5 minutes, what happens to an order placed at minute 2? Verify the synchronous version fails the order and the async version processes it late but successfully.

  5. Confirm the providers are registered. You'll need these later — check now:

    az provider show -n Microsoft.ServiceBus --query registrationState -o tsv
    az provider show -n Microsoft.EventGrid --query registrationState -o tsv
    

    If either prints NotRegistered, register it (this can take a minute):

    az provider register -n Microsoft.EventGrid
    

    Verify: both eventually print Registered. This is a one-time per-subscription setup and won't cost anything.

  6. Confirm you're starting clean. List any existing messaging resources so you know what's yours by the end of the track:

    az servicebus namespace list -o table
    az eventgrid topic list -o table
    

    Verify: note anything already present (probably nothing) so a stray namespace you forgot to delete later is obvious.

  7. Map the concepts to services. Without looking at the table above, write out which Azure service you'd reach for given: a work queue shared by five worker replicas; one OrderPlaced event that three different teams each react to; a firehose of clickstream events you need to replay from yesterday. Then check your answers against the mapping table.

  8. Restate the core trade. In two sentences and no jargon, write down what an event-driven system gives up and what it gets in return. Verify your answer mentions eventual consistency on the "gives up" side and at least two of resilience / independent scaling / load leveling on the "gets" side.

Independent challenge

Take one real workflow you understand — an e-commerce checkout, a CI pipeline, or a support-ticket lifecycle — and design its event-driven decomposition on paper only. Identify at least five distinct events (past-tense names), decide for each which downstream services would react to it, mark every place a duplicate delivery could cause a wrong side effect, and state how you'd make that spot idempotent. Then mark the one or two steps that genuinely cannot be eventually consistent and must stay synchronous commands, and say why. This draws purely on this module plus the command-vs-event and coupling ideas; no Azure resources are involved, so there is nothing to clean up — but keep your diagram, because the capstone (module 08) will ask you to build a system with exactly this shape.

Stuck? One hint

Start from the side effects, not the events. List every irreversible thing the workflow does (charge a card, send an email, ship a box), because those are exactly the points where a duplicate hurts and where idempotency is non-negotiable. The events then fall out naturally as the past-tense facts that trigger those side effects, and the "must be synchronous" steps are the ones where a human or another system is waiting on the answer right now.

Common mistakes & troubleshooting

  • Naming events like commands. ProcessOrder as an event name invites a consumer to think it owns the action. Past-tense (OrderProcessed) keeps the fact/request distinction visible and prevents accidental coupling.
  • Assuming exactly-once from the broker. No mainstream broker gives you true end-to-end exactly-once; assuming it means skipping idempotency and shipping double-charge bugs. Assume at-least-once, always.
  • Confusing "eventually consistent" with "eventually correct if I'm lucky." Eventual consistency is a guarantee that the system converges given the events are processed — it is not an excuse to skip reliability. The convergence only happens because of dead-lettering and retries (modules 05, 07).
  • Using events where you needed an answer now. If the caller must know the result before proceeding (card valid? seat available?), an event is the wrong tool — that's a command/synchronous call. Forcing it async adds latency and complexity for no benefit.
  • Cost pitfall (looking ahead). These concepts are free, but the instinct they should build is that every event you publish later is a billable operation on Service Bus/Event Grid. Designing chatty, fine-grained events ("cursor moved") instead of meaningful business events is both bad architecture and a real cost multiplier — a lesson modules 02 and 03 make concrete.

Checkpoint quiz

Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.

  1. In one sentence each, what's the difference between a command and an event, and how does the past/present tense of the name reflect it?
  2. Define at-most-once, at-least-once, and exactly-once delivery. Which one do real brokers give you, and how do you approximate the one everyone wants?
  3. What does it mean for a consumer to be idempotent, and what's the most common technique to make one idempotent?
  4. Event-driven systems trade one property away to gain others. Which property, and name two things you gain.
  5. PaymentService is down for 5 minutes. Contrast what happens to an in-flight order under a synchronous command call vs. an event on a durable queue.
  6. You publish OrderPlaced and three teams react to it. Is that point-to-point or publish/subscribe? What about five worker replicas pulling from one job queue?
  7. Give one workflow step that should stay a synchronous command even in an otherwise event-driven system, and say why.
Show answers
  1. A command is an imperative request to do something, addressed to one handler and expecting execution (ChargeCard); an event is a past-tense statement that something already happened (CardCharged), addressed to a topic, with any number of reactors. Past tense = it's a fact; present imperative = it's a request.
  2. At-most-once: 0 or 1 deliveries, can lose messages. At-least-once: 1 or more, can duplicate. Exactly-once: no loss, no duplicates. Real brokers give at-least-once; you approximate exactly-once ("effectively-once") with at-least-once delivery plus an idempotent consumer.
  3. Idempotent means processing the same event twice has the same effect as once. The most common technique is a dedup key: each event carries a stable unique id the consumer records (ideally in the same transaction as the side effect) and checks on arrival, skipping ones it's already seen.
  4. It trades away immediate/strong consistency (the system is only eventually consistent). In return you gain resilience (consumer outages don't break the publisher), independent scaling, and load leveling (queues absorb spikes) — any two.
  5. Synchronous: the call blocks and fails, so the order fails at minute 2. Async on a durable queue: the publisher writes the event and succeeds immediately; the consumer processes it once it recovers around minute 5 — late, but successful.
  6. Three teams reacting to OrderPlaced is publish/subscribe (each gets its own copy). Five replicas pulling from one job queue is point-to-point / competing consumers (each message goes to exactly one replica).
  7. Anything where the caller needs the result before continuing — e.g. validating a payment card or checking real-time seat availability at checkout. Making it async would add latency and force the user to wait for an answer the event model can't give synchronously.

Next

01-queues-vs-topics-vs-streams — the same three delivery topologies you just met (point-to-point, pub/sub, ordered log), now mapped onto the concrete Azure services so you know which one to reach for and why.