Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Eventual consistency is not a bug — it is a deliberate engineering trade that you make when the cost of strong consistency (locking, synchronous replication, reduced write throughput) exceeds the cost of briefly serving stale data. But 'eventually consistent' is not the same as 'sometimes wrong' — the lag window has a real duration, real causes, and real consequences that you must design around. Systems that don't acknowledge the lag window ship bugs where a user creates a post and immediately sees it missing from their feed, or worse, where a payment is confirmed but the account balance projection hasn't caught up. Understanding the lag window lets you build UI patterns, retry logic, and reconciliation jobs that make eventual consistency invisible to users.
Simulating a 100ms projection lag shows the read-your-own-writes failure mode and three strategies to handle it.
Use these three in order. Each builds on the one before.
In one paragraph, explain what eventual consistency means and what 'the lag window' refers to in a CQRS system.
Walk me through the read-your-own-writes problem in a CQRS system step by step — what the user does, what each database contains at each moment, and why the read fails.
Given a checkout flow where a payment write and a balance projection must be consistent from the user's perspective, what strategies prevent the user from seeing a stale balance without making the payment write synchronously update both stores?
package main
import (
"fmt"
"sync"
"time"
)
// Simulated store: write to "pg", async projection to "redis".
var (
pgStore = map[string]string{} // source of truth
redisStore = map[string]string{} // eventually consistent
mu sync.Mutex
)
func writePost(id, title string) {
mu.Lock()
pgStore[id] = title
mu.Unlock()
fmt.Printf("[%s] Committed to PostgreSQL: %s\n", time.Now().Format("15:04:05.000"), id)
// Async projection — 100ms simulated lag.
go func() {
time.Sleep(100 * time.Millisecond)
mu.Lock()
redisStore[id] = title
mu.Unlock()
fmt.Printf("[%s] Projection updated in Redis: %s\n", time.Now().Format("15:04:05.000"), id)
}()
}
// Strategy 1: Read from PostgreSQL immediately after write (strong read).
func readStrong(id string) (string, bool) {
mu.Lock()
defer mu.Unlock()
v, ok := pgStore[id]
return v, ok
}
// Strategy 2: Read from Redis with a client-side optimistic cache.
func readWithOptimisticCache(id, justWrittenTitle string, fromCache bool) string {
if fromCache {
return justWrittenTitle + " (from optimistic cache)"
}
mu.Lock()
defer mu.Unlock()
return redisStore[id]
}
// Strategy 3: Poll Redis until the item appears or timeout.
func readWithPolling(id string, timeout time.Duration) (string, bool) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
mu.Lock()
v, ok := redisStore[id]
mu.Unlock()
if ok {
return v, true
}
time.Sleep(10 * time.Millisecond)
}
return "", false
}
func main() {
writePost("p1", "Hello Eventual Consistency")
// Immediately after write — Redis hasn't caught up yet.
v, ok := readStrong("p1")
fmt.Println("Strong read (pg):", v, ok) // present immediately
// 10ms later — Redis projection not ready.
time.Sleep(10 * time.Millisecond)
fmt.Println("Redis read at 10ms:", redisStore["p1"]) // empty — lag window
// Poll strategy — waits up to 200ms.
if v, ok := readWithPolling("p1", 200*time.Millisecond); ok {
fmt.Println("Polling read:", v) // present after ~100ms
}
}go run main.go