Phase 10 — Event-Driven: Event Grid, Service Bus & Logic Apps
Difficulty: ⭐⭐⭐⭐☆ (the mechanism) → ⭐⭐⭐⭐⭐ (the delivery-semantics judgment)
Estimated Time: 1.5 weeks (16–22 hours)
Prerequisites: Phase 03 (Entra ID — the identity a publisher/consumer authenticates with),
Phase 09 (REST/JSON + AuthZ — the shape of a webhook handler), and a working mental model of
idempotency from Phase 01 (ARM PUT) and Phase 02 (Terraform apply). This phase is where
those threads converge into distributed-systems delivery guarantees.
Why This Phase Exists
Up to here the curriculum has been about the control plane (ARM, Terraform, RBAC, landing zones, networking) and synchronous request/response (the API gateway in P09). But the platforms the JD describes are event-driven: a service emits an event, and one or more other services react — asynchronously, decoupled, at their own pace. The moment you decouple producer from consumer with a network and a buffer in between, you have signed up for the hardest problems in distributed systems: messages get lost, duplicated, reordered, and redelivered, and "it worked in the demo" tells you nothing about what happens when a consumer crashes mid-process at 50,000 messages/second.
A senior engineer wires up a Service Bus queue and an Event Grid subscription. A principal understands the delivery state machine underneath well enough to answer the only questions that matter:
- When a consumer pulls a message and then crashes, what happens to that message? (the peek-lock + lock-expiry redelivery answer)
- When the same event is delivered twice, does the system double-charge a customer? (the idempotency + exactly-once-effect answer)
- When a message can never be processed, where does it go and who gets paged? (the max-delivery → dead-letter-queue answer)
- When you need strict order, what does that cost you in throughput? (the sessions answer)
- Should this be a queue, a topic, a stream, or a reactive event? (the Service Bus vs Event Hubs vs Event Grid answer — the most common architecture mistake in the whole space)
The JD names it directly: "Design and implement event-driven architectures using Azure Event Grid, Azure Service Bus, and Azure Logic Apps." This phase makes you build the two delivery engines — the Service Bus broker and the Event Grid retry-delivery path — so the guarantees become arithmetic and a state machine you can draw, not a feature you hope works.
Concepts
- Broker vs router vs log — the three event-plane shapes, and the single decision that
picks one:
- Azure Service Bus — an enterprise message broker. A consumer pulls a message, locks it (peek-lock), processes, and settles it (complete/abandon/dead-letter). Queues (point-to-point) and topics/subscriptions (pub/sub with filters). The home of ordering (sessions), duplicate detection, scheduled/deferred messages, transactions, and dead-letter queues. Use it for commands and workflow steps that must not be lost and may need order or transactions.
- Azure Event Grid — a reactive event router. The publisher fires a small CloudEvents-shaped notification ("a blob was created", "a resource changed") and Event Grid pushes it to subscribers' webhooks/handlers, at-least-once, with exponential-backoff retry and dead-lettering to storage. Use it for "X happened, react" fan-out at massive scale with no consumer to manage.
- Azure Event Hubs — a high-throughput partitioned log (Kafka-shaped): millions of events/second, consumers track offsets, events are replayable within a retention window. Use it for telemetry/streaming/analytics ingestion, not discrete commands. (Named here for the contrast; built fully in the streaming track.)
- Peek-lock delivery — the receive-then-settle state machine:
receivelocks a message invisible forLockDuration; the consumer mustcomplete(remove),abandon(release now), ordead_letterit before the lock expires. Lock expiry → redelivery +DeliveryCount++; once a message has been deliveredMaxDeliveryCounttimes it moves to the dead-letter queue (DLQ) instead of being redelivered. This is the crash-safety mechanism: a dead consumer's message comes back automatically. - Delivery semantics — the spectrum every async system lands on:
- at-most-once — fire-and-forget; no retry; may lose (Service Bus
ReceiveAndDelete, UDP-style). - at-least-once — retry until acknowledged; may duplicate (peek-lock without settle-on-success-only, Event Grid push). The practical default.
- exactly-once-effect — at-least-once plus an idempotent or de-duplicating consumer so a duplicate is a no-op. The only "exactly-once" that exists across a network boundary, and a per-data-product dial, not a free platform setting.
- at-most-once — fire-and-forget; no retry; may lose (Service Bus
- Idempotency keys & duplicate detection — a producer stamps a
MessageId; Service Bus drops a repeat within the dedup window; a consumer keys side effects on it so a redelivery is harmless. The same idea as ARM's idempotentPUT, now on the data plane. - Sessions / FIFO — ordering is not free: a session locks all messages of one
SessionIdto a single consumer, in order, trading parallelism for guaranteed sequence. - CloudEvents — the CNCF interoperability envelope (
id,source,type,subject,time,data) Event Grid speaks, so events are portable across clouds and tools. - Logic Apps — serverless workflow orchestration (a trigger + a graph of connector actions) — the low-code integration complement to code-first Functions (P11). It is how "when an event arrives, call these five SaaS systems in order with retries" gets built without a service.
Labs
Lab 01 — Messaging Broker (flagship, implemented)
| Field | Value |
|---|---|
| Goal | Build the two delivery engines: a Service Bus-style Broker (peek-lock receive, complete/abandon/dead-letter, lock-expiry redelivery with DeliveryCount++, max-delivery → DLQ, duplicate detection, scheduled messages, FIFO sessions with a single active consumer) and an Event Grid-style deliver_with_retry (at-least-once push with deterministic exponential-backoff retry, then dead-letter), plus a classify_delivery semantics map — all offline with time injected |
| Concepts | Peek-lock state machine; lock-expiry redelivery off-by-one; max-delivery dead-lettering; dedup window; scheduled messages; session FIFO; at-least-once + idempotency = exactly-once-effect; exponential backoff |
| Steps | 1. Message + validation; 2. Broker.send (dedup + scheduled + session rules); 3. receive peek-lock + FIFO/session selection; 4. complete/abandon/dead_letter + _requeue_or_dead_letter; 5. tick lock-expiry redelivery + DLQ; 6. deliver_with_retry backoff + dead-letter; 7. classify_delivery |
| How to Test | pytest test_lab.py -v — dedup drop+allow-after-window, peek-lock invisibility + lock-expiry boundary, complete/abandon, max-delivery → DLQ (via abandon and lock-expiry), scheduled-not-early, session FIFO + single-active-consumer, Event Grid schedule + dead-letter + raises-as-failure, classifier mapping, determinism |
| Talking Points | "A consumer crashes mid-process — walk me through what happens to the message." "Is this exactly-once? Prove it." "Queue or topic or Event Grid or Event Hubs — and why?" "What does MaxDeliveryCount actually guard against?" "Why does ordering cost throughput?" |
| Resume bullet | Built a Service Bus-style broker (peek-lock, sessions, duplicate detection, dead-letter) and an Event Grid-style at-least-once retry-delivery engine, modeling the delivery-semantics guarantees behind a production event-driven platform |
→ Lab folder: lab-01-messaging-broker/
Integrated-Scenario Suggestions
These carry the phase into the rest of the track — each is a real event-platform problem the delivery engines underpin:
- Order-processing pipeline — an
OrderPlacedcommand flows Service Bus queue → inventory consumer → payment consumer, each step idempotent onOrderId, poison orders to the DLQ with an alert, and a Logic App fanning notifications. Which steps need sessions (per-customer ordering)? Where do you re-earn exactly-once at each boundary? (Uses the whole lab; builds on P09 auth, P12 Key Vault for the payment secret.) - Reactive blob-processing fan-out —
Microsoft.Storage.BlobCreated→ Event Grid → N subscribers (thumbnailer, virus-scan, indexer), each filtered bysubjectprefix, with dead-letter-to-storage and retry budgets. When does at-least-once double-process a blob, and how does an idempotent handler fix it? (Builds on P07 containers, P11 Functions.) - Telemetry vs commands split — the same product emits both high-volume device telemetry and discrete control commands. Route telemetry to Event Hubs (partitioned log, replay) and commands to Service Bus (ordered, transactional, DLQ). Defend the split with the read:write ratio and the loss/duplicate cost per stream.
- Dead-letter triage runbook — given a DLQ filling at 200 msg/min, write the playbook:
classify the failure (poison payload vs downstream outage vs lock-too-short), decide
replay vs discard, and fix the root cause (raise
MaxDeliveryCount? lengthenLockDuration? add idempotency?). (Builds on P13 observability, P14 reliability.) - Saga / workflow orchestration — a multi-system business process (book flight → charge card → email) as a Logic App (or Durable Function, P11) with compensation steps, retries, and a timeout, sitting on top of Service Bus for the durable steps. Where does the orchestration state live, and how does a replay stay deterministic?
Guides in This Phase
- HITCHHIKERS-GUIDE.md — the 30-minute orientation; read first
- WARMUP.md — the full primer; read slowly
- BROTHER-TALK.md — the candid version
Where This Phase Sits in the Track
This phase is the asynchronous backbone: it turns the synchronous request/response world of P09 into the decoupled, buffered, retry-driven platform the business actually runs on.
| Connects to | How |
|---|---|
| P03 — Entra ID | a publisher/consumer authenticates with a Managed Identity (P12); RBAC data-plane roles (Azure Service Bus Data Sender/Receiver, EventGrid Data Sender) gate send/receive |
| P09 — REST APIs & AuthZ | an Event Grid subscriber is a webhook — a JWT-validated, idempotent HTTP handler exactly like the gateway taught |
| P11 — Serverless / Functions | a Service Bus / Event Grid trigger invokes a Function; the scale controller sizes instances from the queue backlog this phase produces; Durable Functions orchestrate the multi-step sagas |
| P12 — Key Vault & Managed Identity | the secret-free identity a consumer uses, and where a payment/credential in a workflow step is fetched |
| P13 — Observability | DeliveryCount, active/dead-letter message counts, and retry rates are the alerts that catch a stuck consumer before customers do |
| P14 — Reliability | retry + backoff + jitter, the DLQ as a failure bulkhead, and the at-least-once/idempotency contract are the reliability primitives applied to messaging |
| P15 — Capstone | the event-driven, serverless, secure platform is this phase's broker + Event Grid + Functions wired together under a landing zone |
The mental model to carry forward: decoupling is a buffer plus a delivery guarantee, and the guarantee is at-least-once unless you engineer the duplicate away. Every later phase that reacts to something is standing on one of the two engines you build here.
Key Takeaways
- Service Bus is a broker (pull + lock + settle), Event Grid is a router (push + retry), Event Hubs is a log (stream + offsets). Picking the wrong one is the most common — and most expensive — event-architecture mistake. Match the shape to the workload.
- Peek-lock is the crash-safety mechanism: receive locks a message invisible; settle it,
or the lock expires and it's redelivered with
DeliveryCount++; afterMaxDeliveryCountdeliveries it dead-letters. A dead consumer never silently loses a message — that's the whole point. - The default guarantee is at-least-once, which means duplicates happen. "Exactly-once" end-to-end is a myth across a network; what you build is at-least-once + an idempotent / de-duplicating consumer = exactly-once-effect, chosen per data product by what a duplicate (or a loss) actually costs.
- Ordering is not free: a session trades parallelism for FIFO. Don't pay for ordering a stream doesn't need.
- Duplicate detection (
MessageId) and idempotency keys are the same idea as ARM's idempotentPUT, now on the data plane — stamp the producer, key the consumer, and a redelivery becomes a no-op. - The DLQ is a bulkhead, not a graveyard: a poison message that can't be processed must
leave the hot path (so it stops blocking healthy traffic) and raise an alert (so a
human triages it) — that is what
MaxDeliveryCountbuys you.
Deliverables Checklist
-
Lab 01 implemented; all tests pass against
solution.pyand yourlab.py -
You can draw the peek-lock state machine (receive → lock → complete/abandon/
dead-letter; lock-expiry → redeliver +
++; max-delivery → DLQ) from memory - You can state, for a given workload, whether it wants Service Bus, Event Grid, or Event Hubs — and defend it with the read:write ratio and the loss/duplicate cost
-
You can explain why
now == lock_untilcounts as expired (the off-by-one) - You can turn a "we got charged twice" bug into the exactly-once-effect fix (idempotency key on the consumer), and say where the key comes from
- You can name what sessions cost (throughput) and when ordering is actually required
- You can sketch an Event Grid retry policy (backoff schedule, TTL, dead-letter target) and a Logic App workflow that consumes from it