<?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: Mohammad Hossein Karami</title>
    <description>The latest articles on DEV Community by Mohammad Hossein Karami (@mhkarami97).</description>
    <link>https://dev.to/mhkarami97</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F398910%2Ff37fc546-5a13-40d5-9808-857071713f1e.jpg</url>
      <title>DEV Community: Mohammad Hossein Karami</title>
      <link>https://dev.to/mhkarami97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mhkarami97"/>
    <language>en</language>
    <item>
      <title>The Ghost Post: When Users Can't See Their Own Writes</title>
      <dc:creator>Mohammad Hossein Karami</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:41:17 +0000</pubDate>
      <link>https://dev.to/mhkarami97/the-ghost-post-when-users-cant-see-their-own-writes-1kba</link>
      <guid>https://dev.to/mhkarami97/the-ghost-post-when-users-cant-see-their-own-writes-1kba</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1szd8oe9yp12gz7t7hd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1szd8oe9yp12gz7t7hd.png" alt="mhkarami97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You submit a post, refresh the page — it's gone. A second later, it magically appears. You file a bug report. The engineer investigates and finds... nothing wrong.&lt;/p&gt;

&lt;p&gt;This isn't a bug. It's &lt;strong&gt;Read-Your-Writes Consistency&lt;/strong&gt; — one of the most misunderstood distributed systems problems in production today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Read-Your-Writes Consistency?
&lt;/h2&gt;

&lt;p&gt;Martin Kleppmann defines it precisely in &lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"After a user writes data, they should see their own write in subsequent reads — regardless of which replica serves the request."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a &lt;strong&gt;Leader/Follower Replication&lt;/strong&gt; setup, writes always go to the Leader. But reads can be served by any Follower — and that Follower might not have caught up yet. The result: a user's own data becomes temporarily invisible to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Visualized
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User ──── Write ──▶ Leader
                      │
                      │ (async replication, ~500ms lag)
                      ▼
User ──── Read  ──▶ Follower  ← hasn't synced yet → returns stale data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data isn't lost. The system is just eventually consistent — and "eventually" is long enough for users to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Solutions, Ranked by Practicality
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Always Read from Leader
&lt;/h3&gt;

&lt;p&gt;The simplest solution — and the worst at scale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every read hits the Leader, turning it into a &lt;strong&gt;bottleneck&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Followers sit idle, wasting your replication investment&lt;/li&gt;
&lt;li&gt;Falls apart completely in &lt;strong&gt;multi-device scenarios&lt;/strong&gt; (write on mobile, read on desktop hits a different route)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid this unless your system is tiny.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Time Window Routing
&lt;/h3&gt;

&lt;p&gt;After a write, route that user's reads to the Leader for 60 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write → mark user session: "read_from_leader until now() + 60s"
Read  → check session flag → route accordingly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weakness is obvious: &lt;strong&gt;what if replication lag exceeds 60 seconds?&lt;/strong&gt; During heavy load or a network hiccup, you're back to stale reads — and now your window gives false confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. LSN-Based Routing
&lt;/h3&gt;

&lt;p&gt;The Log Sequence Number (LSN) is the Leader's real-time position in the replication stream. Instead of guessing with time, you track &lt;strong&gt;actual replication progress&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write → Leader returns LSN (e.g., 100423)
        Store: lastWriteLSN = 100423

Read  → Only route to a Replica where currentLSN &amp;gt;= 100423
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;position-aware, not time-aware&lt;/strong&gt; — a fundamentally more accurate model. PostgreSQL and MySQL both expose LSN/GTID values you can query directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Commit Token (Oracle BDB Pattern) 🎯
&lt;/h3&gt;

&lt;p&gt;The Leader generates an opaque token after each write. The client holds this token and sends it with every subsequent read. Each Replica checks whether it has processed up to that transaction before serving the response.&lt;/p&gt;

&lt;p&gt;This is the most &lt;strong&gt;precise and portable&lt;/strong&gt; solution — it works across heterogeneous systems and doesn't depend on clock synchronization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-Grade Implementation: Redis + LSN-Based Routing
&lt;/h2&gt;

&lt;p&gt;Combining Commit Tokens with Redis gives you the best balance of &lt;strong&gt;accuracy and scalability&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After Write — store commit position with TTL&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveCommitPositionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;lsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"write_lsn:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StringSetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Before Read — select only a replica that has caught up&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SelectReplicaAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"write_lsn:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lastLsn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StringGetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;lastLsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;GetAnyReplica&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// no recent write, serve normally&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requiredLsn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lastLsn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_replicas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentLsn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentLsnAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentLsn&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;requiredLsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Fallback: no replica ready, hit the Leader&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_leaderConnectionString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Redis?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sub-millisecond reads — no overhead on your hot path&lt;/li&gt;
&lt;li&gt;TTL auto-cleans stale entries (no manual cleanup)&lt;/li&gt;
&lt;li&gt;Horizontally scalable alongside your app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flow summary:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write  →  Leader  →  Get LSN  →  Store in Redis (userId → LSN, TTL 5min)
Read   →  Fetch LSN from Redis  →  Find Replica with LSN &amp;gt;=  →  Route there
                                                               ↘ fallback: Leader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;If you're ignoring this problem, your users are experiencing it — and blaming your app. Research on UX in distributed systems consistently shows that &lt;strong&gt;data disappearing after a user action&lt;/strong&gt; is one of the highest-trust-eroding experiences possible.&lt;/p&gt;

&lt;p&gt;The four solutions form a clear hierarchy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always read Leader&lt;/strong&gt; → simple, doesn't scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Window&lt;/strong&gt; → better, but fragile under lag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSN-Based Routing&lt;/strong&gt; → accurate, requires LSN access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis + LSN (Commit Token)&lt;/strong&gt; → production-ready, scalable, recommended&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any system handling significant concurrent users, the Redis + LSN approach isn't over-engineering — it's the minimum viable guarantee for a trustworthy user experience.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For a working implementation and deeper architectural context, visit &lt;a href="https://blog.mhkarami97.ir/posts/read_write_consistency" rel="noopener noreferrer"&gt;blog.mhkarami97.ir/posts/read_write_consistency&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>programming</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
