Skip to content

Event-driven architecture (EDA) is seductive.

It promises:

  • Loose coupling
  • Independent scaling
  • Reactive workflows
  • Clean separation of concerns

But the production reality is harsher:

Most event-driven systems fail not because of throughput, but because of operational chaos.

  • Duplicate deliveries.
  • Out-of-order events.
  • Poison messages.
  • Replays that double-charge customers.
  • “Invisible” side effects.
  • Debugging that feels like ghost hunting.

This article is about building event-driven systems that survive production: retries, rebalances, network partitions, deployments, and humans.

The core misconception

Many teams think event-driven means:

“Publish events, subscribe to them, done.”

No.

Event-driven means:

“You are building a distributed state machine over unreliable transport.”

If you don’t design for that explicitly, your system will eventually eat itself.

1) The failure modes you must assume will happen

Duplicate delivery (at-least-once is normal)

Kafka, SQS, Pub/Sub, RabbitMQ most real-world setups are at-least-once in practice.

You will see duplicates.

Out-of-order delivery

Retries + parallelism + partitions + consumer group rebalances = reordering.

You cannot assume sequence unless you enforce it.

Partial side effects

A consumer updates the DB, then crashes before committing the offset. Or it calls a payment API, then times out, then retries.

This is where chaos begins.

Replays

Backfills. Topic compaction changes. Bug fixes. New consumers. Disaster recovery.

If replaying events can break your system, your system is fragile.

2) The naïve architecture that breaks

Each consumer does:

  1. Read event
  2. Do side effect
  3. Commit offset

Looks fine until:

  • Payment runs twice → double charge
  • Inventory decrements twice → negative stock
  • Email sends twice → angry customers
  • “Fix” involves manual DB edits → worse

Chaos.

3) The stability rules

Rule 1: Every event must be uniquely identifiable and deduplicable

Every event should carry:

  • eventId (unique per emission)
  • aggregateId (business entity key)
  • version (monotonic per aggregate) or a strict ordering key
  • occurredAt

Example payload:

{
  "eventId": "evt_92af3b2a",
  "type": "OrderPaid",
  "aggregateId": "order_123",
  "version": 3,
  "occurredAt": "2026-02-28T12:00:00Z",
  "data": {
    "paymentId": "pay_987",
    "amount": 4999,
    "currency": "EUR"
  }
}

A consumer must be able to answer: “Have I already processed this exact event?”

Rule 2: State changes and "I processed this event" must be atomic

If you update your DB and later mark the offset (or eventId) as processed, you risk double-processing.

You need a single atomic unit: apply + record.

Rule 3: External side effects must be guarded

Anything outside your DB (emails, payments, SMS, webhooks, third-party calls) must be protected against retries.

Rule 4: Events are immutable facts

Never edit old events. Emit new ones.

Rule 5: Replay safety is the real test

Ask:

If I replay the last 30 days of events, does the system converge to the same state?

If not, you don’t have a reliable system.

4) Publishing without lies: the Transactional Outbox

The classic failure:

  • You write to DB
  • You publish to Kafka
  • Publish fails
  • Now DB says it happened, stream doesn’t

Or vice versa.

The correct pattern

Outbox table (conceptually):

  • id (eventId)
  • aggregateId
  • type
  • payload
  • status (NEW/SENT)
  • created_at
  • sent_at
  • retry_count

This makes your database and your event stream consistent by design.

5) Consuming without chaos: the production-grade consumer loop

The “safe consumer” algorithm

Key points:

  • Dedup check must be fast (often a table keyed by eventId)
  • State transition must be deterministic
  • “Processed eventId” must be committed in the same transaction as the state change

6) Exactly-once: what you can and can’t claim

“Exactly-once semantics” is overloaded marketing.

In practice:

  • Kafka EOS can give you exactly-once between Kafka and Kafka in specific setups
  • Your business still isn’t exactly-once unless your side effects are idempotent

The pragmatic, production truth:

Build for at-least-once delivery + idempotent processing + deterministic state.

That is “exactly-once outcomes”.

7) Handling ordering: versioning beats timestamps

Out-of-order is unavoidable. Your strategy depends on what matters.

Option A: Per-aggregate version

Best when events are state transitions.

Consumer rule:

  • Accept only version == current_version + 1
  • If higher version arrives early → buffer or park
  • If lower version arrives late → ignore (already applied)

Option B: Partition by aggregateId

You can ensure ordering within a partition if you partition by aggregateId.

This helps, but rebalances and retries still mean: duplicates remain.

8) The “poison message” problem: DLQ is not a trash can

Most teams treat DLQ as a graveyard. That’s operational malpractice.

DLQ must be:

  • Alerting (page someone when it grows)
  • Inspectable (payload, headers, failure cause)
  • Replayable (after fix)
  • Rate-limited (avoid floods)
  • Auditable (especially in regulated domains)

A sane message lifecycle

Rule of thumb:

  • Transient failures → retry with backoff
  • Permanent failures (schema mismatch, invariant violation) → DLQ fast

9) Side effects: don’t do them inline unless you can afford pain

External calls are the #1 chaos amplifier.

If you do: consume → call provider → then commit

You’ll eventually cause:

  • double charges
  • repeated emails
  • inconsistent third-party state

The stable pattern: “persist intent, then execute”

This makes side effects:

  • retryable
  • observable
  • independently scalable
  • safe on replays

10) Replay safety: design for time travel

Replays will happen. Plan for them.

Your replay contract

A replay should:

  • not re-send emails unless explicitly configured
  • not re-charge cards
  • not re-decrement inventory
  • converge to the same final state

Two replay modes (be explicit)

  • State rebuild: recompute projections from event stream (safe default)
  • Action replay: re-trigger external effects (dangerous, requires strict idempotency + explicit operator intent)

If you don’t separate these modes, you’ll replay into disaster.

11) Schema evolution: how teams silently break production

You will change event payloads.

If you do it casually, consumers will crash, DLQ will fill, and you’ll be in incident mode.

Compatibility rules

  • Add fields freely (consumers ignore what they don’t know)
  • Never remove or rename fields without versioning
  • Treat required → optional carefully
  • Use a schema registry where possible

Versioning strategies

Either:

  • consumers handle multiple versions, or
  • a translation layer normalizes events

12) Observability: the layer that prevents panic

Event-driven systems become operational chaos when you can’t see them.

You must track:

  • consumer lag per group
  • processing time percentiles
  • retry counts
  • DLQ rate and age
  • dedup hits (duplicate rate is a signal)
  • outbox backlog (publisher health)

The basic “where is it stuck?” map

If you cannot answer “where is it stuck?” in under 60 seconds, you will suffer during incidents.

13) The stable reference architecture

Here’s the architecture that scales without chaos:

Properties:

  • Outbox on the producer
  • Idempotent consumer with eventId dedup
  • Deterministic transitions with ordering checks
  • Side effects executed via persistent tasks
  • DLQ is monitored, replayable, and disciplined
  • Replay safety is a first-class design goal

14) When not to use event-driven

Don’t adopt EDA because it "feels modern".

Avoid event-driven if:

  • You need strict synchronous consistency
  • You have 1–2 services and no scaling pain
  • You can’t commit to observability + on-call readiness
  • Your team is new to distributed failure modes

Event-driven doesn’t reduce work.

It moves work from product code to operations and correctness.

The mental model to keep you sane

Treat your EDA system as:

A replicated asynchronous state machine with unreliable delivery and infinite retries.

If you design for:

  • duplicates
  • reordering
  • retries
  • replays
  • schema evolution
  • observable operations

you get the benefits of EDA without the operational nightmare. If you assume "it won’t happen": It will.

Need this level of clarity for your own software system?
Subera helps companies plan, build, integrate, modernize, and maintain dependable software platforms.
Where Subera can help

Typical work around topics like this

Technical direction
Clarify architecture, risks, tradeoffs, and delivery priorities.
Software delivery
Build systems, workflows, integrations, APIs, and platforms.
Platform evolution
Improve reliability, maintainability, observability, and scale.