DEV Community

Cover image for DDD in Practice: Handling Eventual Consistency Between Aggregates
HASAN
HASAN

Posted on

DDD in Practice: Handling Eventual Consistency Between Aggregates

I recently encountered a consistency issue in an ATS (Applicant Tracking System) that reminded me why good software architecture matters.

Let me share the problem. 👇

🔍 The Bug: Two Aggregates, Two Different Stories

Our ATS had two separate aggregates:

🔹 Application — tracks the candidate's overall status:
Applied → Selecting → Offered → Joined

🔹 SelectionStep — tracks individual recruitment stages, including isInProgress.

The relationship was one-way: SelectionStep knew its Application, but Application didn't track its SelectionSteps.

Now imagine this scenario:

✅ The candidate completes the final interview.
✅ The Application status changes to Offered.
❌ The final SelectionStep still has isInProgress = true.

The result?

The UI could show "Offer accepted" alongside "Final interview in progress." 😅

Both pieces of data were valid individually, but together they told a contradictory story.

💭 What would you do?

Before reading further, think about it:

A) Update SelectionStep directly inside the Application's business logic.

B) Introduce a message broker such as Kafka or RabbitMQ.

C) Use the Outbox Pattern to handle the state change reliably.

I'd love to know your approach! 👇

⚠️ Why not simply update both aggregates?

Calling SelectionStep update directly from Application logic creates tight coupling.

If the second update fails after the first succeeds, we can end up with inconsistent data. And now one aggregate's operation depends on another aggregate's success.

We needed a solution that preserved separation of concerns without losing important events.

✅ Our Approach: The Outbox Pattern

Here's how we approached it:

1️⃣ Atomic Transaction

Save the Application state change and the corresponding domain event in the same database transaction.

If the transaction rolls back, both changes roll back.

2️⃣ Background Outbox Relay

A background process polls the domain_event_outbox table every 10 seconds and dispatches pending events to their handlers.

3️⃣ Retry with Exponential Backoff

If processing fails, schedule another attempt with an increasing delay rather than retrying aggressively.

4️⃣ Dead-Letter Handling

After the configured retries are exhausted, move the event to domain_event_dead_letter for investigation and manual replay after the underlying issue is fixed.

5️⃣ Idempotent Event Handling

Events may be delivered more than once, so the handler must safely handle duplicate processing.

This is essential because a crash can happen even after the handler has performed its update but before the relay records successful processing.

💡 The Bigger Lesson

We didn't need to introduce a new message broker just to solve this particular problem.

The Outbox Pattern allowed us to build reliable, eventually consistent communication between aggregates using our existing relational database.

But there's an important trade-off: eventual consistency means the UI might briefly show stale data. If immediate consistency is a strict business requirement, the design needs additional consideration.

Good architecture isn't always about adding more infrastructure.

Sometimes, it's about using the right pattern to solve the actual problem.

👇 Let's discuss!

I'm curious about your experience:

Have you ever encountered inconsistent data between aggregates, services, or database tables?

  • Did you use the Outbox Pattern?
  • Did you choose Kafka, RabbitMQ, or a simpler database-backed solution?
  • How did you handle retries, duplicate events, and failed processing?

Share your approach in the comments. I'd love to learn how other engineers tackle this challenge! 🚀

Top comments (0)