<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HASAN</title>
    <description>The latest articles on DEV Community by HASAN (@me-hasan).</description>
    <link>https://dev.to/me-hasan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F180651%2F33e4f9df-fcca-4be3-8849-713b82b055ce.jpeg</url>
      <title>DEV Community: HASAN</title>
      <link>https://dev.to/me-hasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/me-hasan"/>
    <language>en</language>
    <item>
      <title>DDD in Practice: Handling Eventual Consistency Between Aggregates</title>
      <dc:creator>HASAN</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:52:38 +0000</pubDate>
      <link>https://dev.to/me-hasan/ddd-in-practice-handling-eventual-consistency-between-aggregates-429</link>
      <guid>https://dev.to/me-hasan/ddd-in-practice-handling-eventual-consistency-between-aggregates-429</guid>
      <description>&lt;p&gt;I recently encountered a consistency issue in an ATS (Applicant Tracking System) that reminded me why good software architecture matters.&lt;/p&gt;

&lt;p&gt;Let me share the problem. 👇&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 The Bug: Two Aggregates, Two Different Stories
&lt;/h3&gt;

&lt;p&gt;Our ATS had two separate aggregates:&lt;/p&gt;

&lt;p&gt;🔹 &lt;strong&gt;Application&lt;/strong&gt; — tracks the candidate's overall status:&lt;br&gt;
&lt;code&gt;Applied → Selecting → Offered → Joined&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🔹 &lt;strong&gt;SelectionStep&lt;/strong&gt; — tracks individual recruitment stages, including &lt;code&gt;isInProgress&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The relationship was one-way: SelectionStep knew its Application, but Application didn't track its SelectionSteps.&lt;/p&gt;

&lt;p&gt;Now imagine this scenario:&lt;/p&gt;

&lt;p&gt;✅ The candidate completes the final interview.&lt;br&gt;
✅ The Application status changes to &lt;code&gt;Offered&lt;/code&gt;.&lt;br&gt;
❌ The final SelectionStep still has &lt;code&gt;isInProgress = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;The UI could show &lt;strong&gt;"Offer accepted"&lt;/strong&gt; alongside &lt;strong&gt;"Final interview in progress."&lt;/strong&gt; 😅&lt;/p&gt;

&lt;p&gt;Both pieces of data were valid individually, but together they told a contradictory story.&lt;/p&gt;

&lt;h3&gt;
  
  
  💭 What would you do?
&lt;/h3&gt;

&lt;p&gt;Before reading further, think about it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A)&lt;/strong&gt; Update SelectionStep directly inside the Application's business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B)&lt;/strong&gt; Introduce a message broker such as Kafka or RabbitMQ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C)&lt;/strong&gt; Use the Outbox Pattern to handle the state change reliably.&lt;/p&gt;

&lt;p&gt;I'd love to know your approach! 👇&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Why not simply update both aggregates?
&lt;/h3&gt;

&lt;p&gt;Calling SelectionStep update directly from Application logic creates tight coupling.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;We needed a solution that preserved separation of concerns without losing important events.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Our Approach: The Outbox Pattern
&lt;/h3&gt;

&lt;p&gt;Here's how we approached it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1️⃣ Atomic Transaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Save the Application state change and the corresponding domain event in the same database transaction.&lt;/p&gt;

&lt;p&gt;If the transaction rolls back, both changes roll back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2️⃣ Background Outbox Relay&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A background process polls the &lt;code&gt;domain_event_outbox&lt;/code&gt; table every 10 seconds and dispatches pending events to their handlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3️⃣ Retry with Exponential Backoff&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If processing fails, schedule another attempt with an increasing delay rather than retrying aggressively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4️⃣ Dead-Letter Handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the configured retries are exhausted, move the event to &lt;code&gt;domain_event_dead_letter&lt;/code&gt; for investigation and manual replay after the underlying issue is fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5️⃣ Idempotent Event Handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Events may be delivered more than once, so the handler must safely handle duplicate processing.&lt;/p&gt;

&lt;p&gt;This is essential because a crash can happen even after the handler has performed its update but before the relay records successful processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  💡 The Bigger Lesson
&lt;/h3&gt;

&lt;p&gt;We didn't need to introduce a new message broker just to solve this particular problem.&lt;/p&gt;

&lt;p&gt;The Outbox Pattern allowed us to build reliable, eventually consistent communication between aggregates using our existing relational database.&lt;/p&gt;

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

&lt;p&gt;Good architecture isn't always about adding more infrastructure.&lt;/p&gt;

&lt;p&gt;Sometimes, it's about using the right pattern to solve the actual problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  👇 Let's discuss!
&lt;/h3&gt;

&lt;p&gt;I'm curious about your experience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you ever encountered inconsistent data between aggregates, services, or database tables?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did you use the Outbox Pattern?&lt;/li&gt;
&lt;li&gt;Did you choose Kafka, RabbitMQ, or a simpler database-backed solution?&lt;/li&gt;
&lt;li&gt;How did you handle retries, duplicate events, and failed processing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Share your approach in the comments. I'd love to learn how other engineers tackle this challenge! 🚀&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>ddd</category>
      <category>outboxpattern</category>
      <category>backendengineering</category>
    </item>
  </channel>
</rss>
