← Back to blog
·3 min read

Event-driven by default: design for the failure modes first

In event-driven AWS systems, duplicate and out-of-order events are the normal case, not edge cases. Design for them first, then make the happy path fast.

cloud-architectureevent-drivenaws-serverless

In an event-driven system on AWS, the failure modes — duplicate delivery, out-of-order events, downstream outages, poison messages — are not edge cases. They are the normal operating condition. Designing for them first, before the happy path, is what separates a serverless architecture that survives production from a demo that falls over under load.

The problem

The appeal of event-driven serverless — Lambda, SQS, SNS, EventBridge — is real: decoupled services and elastic scale, almost for free. The trap is that the happy path is trivial to build and demos beautifully, so teams build it first and file resilience under "we'll add error handling later."

But event-driven systems fail differently from request/response. At-least-once delivery means every consumer will see the same message twice — not rarely, routinely. Async fan-out means events arrive out of order relative to how they were produced. A downstream outage doesn't return a clean 500; it makes messages pile up until a naive consumer either drops them or reprocesses them into corruption. And a single malformed event can wedge a consumer that retries it forever. Under real load these aren't tail risks — they happen every day, and "later" arrives as an incident.

What we did

The decision that matters is design for the failure modes first, then make the happy path fast — the same principle behind our cloud architecture work. In practice that means four things are non-negotiable before the first consumer ships:

  • Idempotency as a first-class requirement. Every event carries a stable id, and every consumer is keyed on it, so a re-delivered message is a no-op instead of a double-charge.
  • A dead-letter queue on every queue and subscription, from day one, with a redrive path. A message that can't be processed parks for inspection instead of blocking the stream or vanishing silently.
  • Order-tolerance by default; strict ordering only where the domain demands it. Global ordering is expensive (FIFO throughput limits, partition keys) and usually unnecessary — so most handlers are written to tolerate any order, and ordering is spent only where correctness truly needs it.
  • Bounded retries with backoff, then DLQ. One poison message can't pin the consumer, and DLQ depth is alarmed so a growing backlog pages someone.

The alternative we explicitly rejected was happy-path first, resilience later. Retrofitting idempotency and dead-lettering after the consumers and the message schema already exist means touching every consumer — and usually the event contract itself. It is far more expensive than designing the event envelope (id, timestamp, version) up front, and it tends to get prioritized only after the first data-corruption incident. This is the kind of trade-off we settle in the design phase, before code, on the systems we've shipped.

The result

The artifact is the mapping we design against: each failure mode has a why and a standard response, decided once and applied everywhere.

Failure-mode table. Duplicate delivery, caused by at-least-once semantics, is handled with an idempotency key and consumer-side dedup. Out-of-order events, caused by async fan-out and parallel consumers, are handled with order-tolerant handlers and FIFO only where needed. Downstream outage, where a consumer cannot complete, is handled with bounded retries and backoff, then a dead-letter queue. Poison message, a malformed or unprocessable event, is handled with a DLQ plus redrive and an alarm on DLQ depth.

Those responses aren't bolted onto the topology afterward; they are the topology. The producer publishes to a bus, each consumer has its own queue with a dead-letter queue behind it, and the redrive path is wired before any business logic exists.

Architecture diagram: a producer publishes events to an SNS/EventBridge bus, which fans out to per-consumer SQS queues; each queue feeds a Lambda consumer and has a dead-letter queue on a bounded-retry path, with DLQ depth alarmed.

Takeaway

In event-driven systems, resilience is not a layer you add — it is the shape of the design. Decide idempotency, dead-lettering, and ordering before the first consumer ships, and the happy path becomes the easy part.

Building something like this?