Skip to content

Event-Driven & Asynchronous Communication

Instead of phoning someone and waiting on hold, you send a text and they reply when they can — that’s the shift from synchronous calls to event-driven communication.

  • What it is: designing communication around events/messages (pub/sub, queues) rather than direct synchronous request/response calls.
  • Why it matters: reduces tight coupling and stops one slow or failing service from freezing everything that calls it; absorbs traffic spikes better.
  • For developers: before wiring up a new integration, ask whether it truly needs an instant answer, or whether an event will do.
  • When to use it: when a caller doesn’t need an immediate response, when decoupling producer and consumer lifecycles matters, or when absorbing bursty load.
  • When not to use it: when the caller genuinely cannot proceed without an immediate answer (e.g. checking real-time payment authorization) — that calls for a synchronous call, not an event.
  • Prerequisites: Service & Component Boundaries.
sequenceDiagram
    participant Warehouse
    participant Bus as Event bus
    participant Storefront
    Warehouse->>Bus: publish InventoryAdjusted
    Bus-->>Storefront: deliver InventoryAdjusted
    Note over Warehouse,Storefront: Warehouse doesn't wait for Storefront to process it
  1. Identify whether the caller needs an immediate response or can proceed without waiting.
  2. If not, define the event: its name (past tense, e.g. InventoryAdjusted), payload, and the delivery guarantee (at-least-once, exactly-once, ordering).
  3. Choose a broker/queue appropriate to the guarantee needed.
  4. Design consumers to be idempotent — the same event may be delivered more than once.
  5. Add monitoring for the event stream (lag, dead-letter queue) so a stuck consumer is visible, not silent.

The storefront currently polls the warehouse system every 5 seconds for stock levels.

  • Input: constant polling adds load to the warehouse system and up to 5 seconds of staleness on the storefront.
  • Process: the warehouse instead publishes an InventoryAdjusted event (SKU, new quantity, timestamp) whenever stock changes; the storefront subscribes and updates its cache on receipt.
  • Output: stock updates propagate within milliseconds of the change instead of on a polling delay; warehouse load drops since it’s no longer answering constant poll requests; the storefront keeps working (with slightly stale data) even if the warehouse is briefly unavailable.
public sealed record InventoryAdjusted(string EventId, string Sku, int NewQuantity, DateTimeOffset OccurredAt);
// Storefront consumer: idempotent on EventId, safe against at-least-once redelivery
async Task HandleAsync(InventoryAdjusted evt)
{
if (await _processedEvents.ContainsAsync(evt.EventId))
return; // already handled this delivery
await _stockCache.UpdateAsync(evt.Sku, evt.NewQuantity);
await _processedEvents.MarkProcessedAsync(evt.EventId);
}

On Azure, the warehouse publishes to an Azure Event Grid topic (or an Azure Service Bus topic if ordered, at-least-once delivery per SKU partition is needed); the storefront’s subscription includes a dead-letter destination so a repeatedly failing event is visible instead of silently dropped.

The storefront’s consumer can run as an Azure Function with a Service Bus trigger, so instance count scales automatically with queue length instead of needing its own autoscale rule:

[Function("StorefrontStockUpdater")]
public async Task Run(
[ServiceBusTrigger("inventory-adjusted", Connection = "ServiceBusConnection")] InventoryAdjusted evt)
{
await HandleAsync(evt);
}

The Function App only needs a small Azure Storage Account for its own runtime/trigger state (AzureWebJobsStorage) — no separately hosted, always-on service to manage for this consumer.

Use case When to use Notes
Notifying other systems of a state change When consumers don’t need to block the producer Use past-tense event names describing what happened
Replacing polling When polling adds load or staleness Publish on change instead of being asked repeatedly
Absorbing bursty load When traffic spikes exceed steady processing capacity A queue lets consumers drain the backlog at their own pace
  • Eventual consistency trade-off: consumers see the change slightly after it happens, not instantly — this must be acceptable for the use case.
  • Idempotency: most brokers guarantee at-least-once delivery, so consumers must handle duplicate events safely (e.g. by keying on an event id).
  • Ordering guarantees vary by broker/partition strategy — don’t assume global ordering unless the technology explicitly provides it for your use case.
Term Meaning
Event A record that something happened, published without expecting an immediate reply
Pub/sub A pattern where producers publish and any number of subscribers receive independently
At-least-once delivery A guarantee that a message will be delivered, possibly more than once
Idempotent consumer A consumer that produces the same result even if it processes the same event twice
Dead-letter queue Where messages go after repeated processing failures, for inspection instead of silent loss
Symptom Likely cause Fix
Duplicate side effects (e.g. double-adjusted inventory) Consumer isn’t idempotent Key processing on a unique event id and skip already-processed ids
Consumers falling behind silently No monitoring on queue lag Add lag/dead-letter monitoring and alerting
Events processed out of order causing incorrect state No ordering guarantee where one was assumed Use a partition key or sequence field the consumer can check
  • Why is polling worse than events for both load and staleness?
  • Name one place a synchronous call is still the right choice, and explain why an event wouldn’t work there.

Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries, API Design & Contracts, Resilience & Observability, and the Glossary.