Section 04

Distributed systems essentials

What changes when one program becomes many machines talking over a network: partial failure, replication, quorums, consensus and leader election, distributed locks, distributed transactions, and why you can't trust the clock. CAP and PACELC are covered in 03.

4.1 Why distributed is hard: fallacies & partial failure Added

On one machine, things either work or crash. Across a network, some parts fail while others keep going, and you often can't tell which.

The 8 fallacies of distributed computing

False assumptions that engineers new to distributed systems make (L. Peter Deutsch and others at Sun Microsystems, 1990s).

FallacyRealityWhat you do
1. The network is reliablePackets drop, connections reset, cables and switches failTimeouts, retries with backoff, idempotency
2. Latency is zeroCross-region calls take tens to hundreds of msFewer, coarser calls; caching; async
3. Bandwidth is infiniteBig payloads clog links and cost moneyPaging, compression, claim check
4. The network is secureAnything on the wire can be read or changedTLS / mTLS, zero trust
5. Topology doesn't changePods, IPs and routes change all the timeService discovery, DNS, health checks
6. There is one administratorMany teams, vendors and clouds own parts of the pathClear contracts, observability, runbooks
7. Transport cost is zeroSerialisation, egress and hops cost CPU and moneyEfficient formats (gRPC/Protobuf), co-location
8. The network is homogeneousMixed hardware, OSs, protocols and versionsOpen standards, versioned APIs

💥 Partial failure

One node, link or dependency fails while the rest keep running. A timeout tells you nothing about whether the work happened.

✂ Network partition

Nodes are alive but can't reach each other. Each side may think the other is dead. This is the "P" in CAP (see 3.11).

🪖 Two generals problem

Two generals must attack together but can only send messengers who may be captured. No finite number of messages makes both certain. Lesson: over an unreliable network you can't get guaranteed agreement, only good-enough protocols.

🐢 Slow = dead?

A GC pause, a busy disk or a noisy neighbour looks exactly like a crash from outside. Failure detection is always a guess based on timeouts.

  • A timeout means "I don't know", not "it failed". Design every remote write to be safe to retry (idempotency, see 8.7).
  • Assume every network call can be slow, fail, or succeed without you hearing about it.
  • The more services in a request path, the more likely one is down. Availabilities multiply.
How to implement it
Library / APIPackageUse it for
Resilience pipelineMicrosoft.Extensions.Http.ResilienceTimeouts, retries with jitter and circuit breakers on every outbound HttpClient call (details in 3.6)
Cancellationbuilt-in CancellationToken, CancellationTokenSource.CancelAfterGive every remote call a deadline and pass it down the chain

"The network is reliable" is the most expensive assumption in software 💸

⏱ A timeout doesn't mean it failed. It means you don't know.

✂ A partition means both sides are alive but think the other is dead.

🐢 A slow node looks exactly like a dead one.

🔁 So every remote write must be safe to retry.

Which of the 8 fallacies has bitten you hardest?

#DistributedSystems #SystemDesign #SoftwareArchitecture #Resilience

4.2 Replication models Added

Keep copies of the same data on several nodes for availability, durability and read scale. The big choices: who accepts writes, and whether to wait for copies.

Single-leaderMulti-leaderLeaderless
Who takes writesOne leader. Followers copy its logSeveral leaders (often one per region) that sync with each otherAny replica. The client writes to several at once
ProsSimple. No write conflictsLocal writes in every region. Survives a region lossNo failover needed. Very high availability
ConsLeader is a bottleneck. Failover takes time and can lose async writesWrite conflicts need resolving (last-write-wins, merge, CRDTs)Weaker consistency. Needs quorums and read repair
ExamplesSQL Server Always On, PostgreSQL streaming replication, MongoDB replica sets, a Kafka partitionCosmos DB multi-region writes, CouchDB, active-active databasesCassandra, ScyllaDB, Riak (Amazon Dynamo paper style)

⏳ Synchronous replication

  • Leader waits for the follower to confirm before saying "done"
  • No data loss on failover (RPO = 0)
  • Slower writes. A slow or dead follower can block writes
  • Common middle ground: wait for one follower (semi-sync), the rest async

⚡ Asynchronous replication

  • Leader confirms right away and ships changes later
  • Fast writes, followers can be far away (other regions)
  • Recent writes can be lost on failover
  • Followers lag behind, so reads can be stale

Replication-lag anomalies (and fixes)

  • Single-leader is the default. Pick multi-leader or leaderless only when you need multi-region writes or extreme availability, and have a conflict plan.
  • Async replication means RPO > 0 and stale reads. Say so explicitly in a design.
  • Route reads that must be fresh (balance after a payment) to the primary. Send reports and search to replicas.
How to implement it
Library / APIPackageUse it for
Cosmos DB regionsMicrosoft.Azure.CosmosCosmosClientOptions.ApplicationPreferredRegions to read from the nearest region; Session consistency gives read-your-writes per client
SQL read replicasMicrosoft.Data.SqlClientApplicationIntent=ReadOnly in the connection string sends reads to a readable secondary
EF Core read/write splitMicrosoft.EntityFrameworkCore.SqlServerTwo DbContext registrations: one on the primary for writes, one on the read-only replica with AsNoTracking
CassandraCassandraCSharpDriverLeaderless replication with per-query consistency (see 4.3)
// Cosmos DB: read from the closest region first, fail over in order
var cosmos = new CosmosClient(endpoint, new DefaultAzureCredential(), new CosmosClientOptions
{
    ApplicationPreferredRegions = [Regions.AustraliaEast, Regions.AustraliaSoutheast]
});

// SQL Server: a second connection string for replica reads
// "Server=tcp:myag-listener;Database=Bank;ApplicationIntent=ReadOnly;..."
builder.Services.AddDbContext<BankDb>(o => o.UseSqlServer(primaryCs));
builder.Services.AddDbContext<BankReadDb>(o => o.UseSqlServer(readOnlyCs)
    .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

"I updated my email, refreshed, and it changed back." Not a bug in your code. It's replication lag 🔁

👑 Single-leader: one node takes writes, followers copy. Simple.

🌏 Multi-leader: write in every region, but now you resolve conflicts.

🕸 Leaderless: write to many, read from many, let quorums decide.

⚡ Async = fast but stale reads and possible loss on failover.

Fix the symptom with read-your-writes: read your own data from the leader for a few seconds.

#DistributedSystems #Databases #SystemDesign #Replication

4.3 Quorum: N, W, R Added

With N replicas, a write must reach W of them and a read must ask R of them. If R + W > N, every read overlaps at least one replica that has the latest write.

N: replicas

How many copies of each item exist (Cassandra calls it the replication factor). Typical: 3.

W: write quorum

How many replicas must confirm a write before it counts as done.

R: read quorum

How many replicas a read asks. The newest version among the answers wins.

N / W / RR + W > N?BehaviourTolerates
3 / 2 / 2✅ 4 > 3Balanced default. Fresh reads1 node down for both reads and writes
3 / 3 / 1✅ 4 > 3Fast reads, slow and fragile writesWrites fail if any node is down
3 / 1 / 3✅ 4 > 3Fast writes, slow readsReads fail if any node is down
3 / 1 / 1❌ 2 ≤ 3Fastest, eventually consistent2 nodes down

Sloppy quorum & hinted handoff

  • If some home replicas are unreachable, a sloppy quorum accepts the write on other nodes so writes stay available.
  • The stand-in node keeps a hint and hands the data back when the home replica returns (hinted handoff).
  • Trade-off: during that window, R + W > N no longer guarantees a fresh read.
  • Described in Amazon's Dynamo paper. Cassandra does hinted handoff, but hints don't count towards the consistency level (except ANY).

In real products

  • Cassandra / ScyllaDB: N = replication factor per keyspace. Pick W and R per query: ONE, QUORUM, LOCAL_QUORUM (majority in your data centre), ALL.
  • Cosmos DB: you don't set N, W or R. Each partition has a 4-replica set; writes commit to a local majority (3 of 4), Strong and Bounded Staleness read from 2 replicas, the other levels read from 1. You choose a consistency level instead (3.12).
  • Raft / Paxos systems (etcd, ZooKeeper) use majority quorums too (4.4).
  • R + W > N → read and write sets overlap. Majority both ways (W = R = ⌊N/2⌋ + 1) is the usual choice.
  • Quorums alone aren't full linearizability: sloppy quorums, concurrent writes and half-failed writes can still surprise you.
  • Use odd N (3, 5) so a majority survives one or two failures.
How to implement it
Library / APIPackageUse it for
Cassandra quorumCassandraCSharpDriverConsistencyLevel.LocalQuorum, Quorum, One, All per statement, or as the default in QueryOptions
Cosmos DBMicrosoft.Azure.CosmosNo N/W/R knobs. Choose ConsistencyLevel on the account; a request can only make it weaker
// Cassandra: RF = 3 per data centre. LOCAL_QUORUM on both sides → 2 + 2 > 3
var cluster = Cluster.Builder()
    .AddContactPoint("cassandra-1")
    .WithQueryOptions(new QueryOptions().SetConsistencyLevel(ConsistencyLevel.LocalQuorum))
    .Build();
var session = await cluster.ConnectAsync("payments");

var insert = new SimpleStatement(
    "INSERT INTO balances (account_id, balance) VALUES (?, ?)", accountId, 50m);
await session.ExecuteAsync(insert);                 // waits for 2 of 3 local replicas

var read = new SimpleStatement("SELECT balance FROM balances WHERE account_id = ?", accountId)
    .SetConsistencyLevel(ConsistencyLevel.One);     // cheaper, may be stale
var rows = await session.ExecuteAsync(read);

One inequality explains how Cassandra-style databases stay consistent: R + W > N 🧮

📦 N = copies of the data (say 3)

✍️ W = copies that must confirm a write (2)

👀 R = copies you ask on a read (2)

🔗 2 + 2 > 3, so every read touches at least one fresh copy

Drop to W = 1, R = 1 and you trade freshness for speed. Which way does your system lean?

#DistributedSystems #Cassandra #Databases #SystemDesign

4.4 Consensus & leader election (Raft) Added

Consensus gets a group of nodes to agree on one value, or one ordered log, even when some nodes crash or messages are lost, as long as a majority is up.

🎯 What it solves

  • Who is the leader right now?
  • What's the agreed order of writes?
  • Is this config change committed?

🔢 Majority rule

2f + 1 nodes survive f failures. 3 nodes survive 1, 5 survive 2. 4 nodes still survive only 1, so use odd numbers.

📜 Paxos in one line

Lamport's original consensus algorithm: proven correct, famously hard to understand and implement (used in Google Chubby and Spanner).

Raft in five ideas

SystemAlgorithmUsed for
etcdRaftStores all Kubernetes cluster state. Leases and elections for controllers
ZooKeeperZAB (ZooKeeper Atomic Broadcast)Coordination for Hadoop, HBase, Solr and older Kafka clusters
ConsulRaftService catalogue, KV store, sessions and locks
Kafka KRaftRaft-based metadata quorumController election and cluster metadata. Kafka 4.0 (March 2025) removed ZooKeeper entirely
CockroachDB, TiKV, YugabyteDBRaft per data rangeEach shard is its own small Raft group

🧠🧠 Split brain

Two nodes both believe they are leader, usually after a partition or a too-short timeout, and both accept writes. Data diverges.

  • Needs a majority to lead, so only one side can win
  • Odd node counts, or a tie-breaker witness (e.g. a cloud witness for a 2-node cluster)

🎫 Fencing tokens

Every new leader or lock holder gets a number that only goes up (Raft term, ZooKeeper zxid, etcd revision). Storage rejects any request with a lower number than it has already seen, so an old leader that wakes up can't do damage. More in 4.5.

  • Don't write your own consensus. Borrow it: use a lease from etcd/Kubernetes, ZooKeeper, Consul or a cloud lock (Azure Blob lease).
  • Consensus needs a majority, so a 3-node cluster with 2 nodes down stops accepting writes. That's the CP choice by design.
  • Leader election is only half the job. The resource the leader writes to must also check a fencing token.
How to implement it
Library / APIPackageUse it for
Kubernetes leader electionKubernetesClient (k8s.LeaderElection)LeaderElector over a Kubernetes Lease object, with OnStartedLeading / OnStoppedLeading events. Run one active replica of a worker
Azure Blob leaseAzure.Storage.Blobs (BlobLeaseClient)Simple leader election outside Kubernetes: whoever holds the blob lease (15–60 s, renewable) is leader
etcd clientdotnet-etcdgRPC client for etcd v3: leases, watches, transactions, election and lock APIs
Raft in .NETDotNext.Net.Cluster, DotNext.AspNetCore.ClusterA full Raft implementation (TCP or HTTP transport) if you really must build a replicated state machine yourself
// Leader election with an Azure Blob lease: only the lease holder runs the job
var lease = container.GetBlobClient("leader/statement-run").GetBlobLeaseClient();
while (!ct.IsCancellationRequested)
{
    try
    {
        await lease.AcquireAsync(TimeSpan.FromSeconds(30), cancellationToken: ct);
        using var renew = new PeriodicTimer(TimeSpan.FromSeconds(10));
        var work = RunAsLeaderAsync(ct);                     // the singleton job
        while (!work.IsCompleted && await renew.WaitForNextTickAsync(ct))
            await lease.RenewAsync(cancellationToken: ct);   // keep leadership
        await work;
        await lease.ReleaseAsync(cancellationToken: ct);
    }
    catch (RequestFailedException e) when (e.Status == 409) // someone else leads
    {
        await Task.Delay(TimeSpan.FromSeconds(15), ct);
    }
}

If a renew fails, stop working immediately: another instance may already be leader. Pair the lease with a fencing check on the data you write.

How does Kubernetes know which etcd node is in charge? Raft 🗳️

💓 The leader sends heartbeats. Silence for too long → a follower runs for election.

🔢 Every election bumps the "term". Higher term always wins.

🏆 A majority of votes makes you leader. 5 nodes survive 2 failures.

📜 A write is committed once a majority has it in their log.

🧟 An old leader that wakes up sees a higher term and steps down.

Rule of thumb: never build your own consensus. Borrow it from etcd, ZooKeeper or a cloud lease.

#DistributedSystems #Raft #Kubernetes #SystemDesign

4.5 Distributed locks Added

A lock shared across machines so that only one worker does something at a time, such as running the nightly statement job or refreshing a shared token.

⏲ Lease, not a lock

Every distributed lock needs a TTL. If the holder crashes, the lock frees itself. The holder must renew before it expires.

🧟 The zombie problem

The holder pauses (GC, VM freeze, slow network) longer than the TTL. The lock expires, someone else takes it, the first one wakes up and still thinks it holds it. Two writers.

🎫 Fencing token

The lock service hands out a number that goes up with every grant. The storage rejects writes carrying an older number.

OptionHowGood forWatch out
Redis single keySET lock:job <id> NX PX 30000; release only if the value is still yours (Lua script)Fast, simple efficiency locks (avoid doing work twice)Lost if Redis fails over before replicating. No fencing token
RedlockTake the key on a majority of N independent Redis mastersEfficiency locks with fewer single points of failureKleppmann (2016) showed it relies on timing assumptions and gives no fencing token; Redis's author disputed this. Don't rely on it for correctness
Azure Blob leaseLease a blob for 15–60 s (or infinite) and renew itAzure-native singleton jobs and leader electionRenew in the background. No built-in token, but the lease ID identifies the holder
SQL Server / PostgreSQLsp_getapplock; pg_advisory_lock / pg_try_advisory_xact_lockYou already have the DB. The lock dies with the session or transactionHolds a connection open. DB failover drops locks
ZooKeeper / etcdEphemeral sequential nodes; etcd lease + lock APICorrectness locks: consensus-backed, natural fencing token (zxid, revision)Another cluster to run

⚖️ Efficiency vs correctness

Ask "what happens if two workers run at once?" If the answer is "some wasted work", Redis is fine. If it's "a customer is charged twice", you need fencing or, better, no lock at all.

✅ Prefer idempotency over locks

  • A unique constraint on a business key (payment ID)
  • Conditional update / optimistic concurrency (rowversion, ETag)
  • One consumer per key (Service Bus sessions, Kafka partitions)
  • Idempotent handlers (8.7)
  • Every distributed lock is really a lease. Assume it can expire while you're still working.
  • Use fencing tokens, or a conditional write, wherever two holders would corrupt data.
  • lock / SemaphoreSlim only work inside one process. They do nothing across pods.
How to implement it
Library / APIPackageUse it for
DistributedLockDistributedLock.SqlServer, DistributedLock.Postgres, DistributedLock.Redis, DistributedLock.Azure, DistributedLock.ZooKeeperOne IDistributedLock API over SQL app locks, Postgres advisory locks, Redis (incl. Redlock), Blob leases and ZooKeeper. Auto-renews while held
Redis primitivesStackExchange.RedisLockTakeAsync(key, value, expiry) / LockReleaseAsync for a simple single-instance lock
RedlockRedLock.netRedLockFactory.CreateLockAsync across several independent Redis servers (last release 2022)
Blob leaseAzure.Storage.Blobs (BlobLeaseClient)Raw lease API: AcquireAsync, RenewAsync, ReleaseAsync
In-process onlybuilt-in SemaphoreSlim, System.Threading.LockGuard code inside one app instance. Not a distributed lock
// One instance runs the job; a fencing check protects the data anyway
var @lock = new SqlDistributedLock("statement-run-2026-10", connectionString);
await using var handle = await @lock.TryAcquireAsync(TimeSpan.Zero, ct);
if (handle is null) return;                        // another pod has it

long token = await jobs.NextFencingTokenAsync(ct);  // e.g. a SQL SEQUENCE
foreach (var batch in await LoadBatchesAsync(ct))
{
    // Only write if no newer holder has written already
    int updated = await db.StatementRuns
        .Where(r => r.Id == batch.RunId && r.LastToken < token)
        .ExecuteUpdateAsync(s => s.SetProperty(r => r.LastToken, token)
                                  .SetProperty(r => r.Status, "Done"), ct);
    if (updated == 0) break;                       // we've been fenced off
}

Your distributed lock is lying to you. It's really a lease 🔒⏲

😴 Worker 1 takes the lock, then freezes in a long GC pause.

⌛ The lease expires. Worker 2 takes it and writes.

🧟 Worker 1 wakes up, still "holding" the lock, and writes too.

🎫 Fix: fencing tokens. Storage rejects any write with an older token.

✅ Better fix: make the operation idempotent so you don't need the lock.

Where do you still rely on a Redis lock for correctness?

#DistributedSystems #Redis #SystemDesign #Concurrency

4.6 Distributed transactions: 2PC vs Saga Added

Two-phase commit (2PC) makes several databases commit or roll back together. It's atomic, but blocking and fragile, so microservices usually use a saga instead.

2PC / XASagaTCC (Try-Confirm-Cancel)
ConsistencyAtomic (ACID across resources)Eventual. Undo with compensating actionsEventual, with reservations
HowPrepare, then commitLocal transaction per step + event; compensate on failureTry reserves, Confirm uses the reservation, Cancel releases it
LocksHeld across all participants until commitShort, local onlyReservation, not a DB lock
AvailabilityLow: every participant and the coordinator must be upHigh: steps can retry laterHigh
FitsA few databases in one data centre that all support XAMicroservices, long-running business flowsInventory, seats, funds holds
Banking exampleLegacy core moving money between two DBs on one platformOpen account → KYC → card issue, with undo stepsCard authorisation (hold funds) then capture or void

Why microservices avoid 2PC

  • 2PC gives atomicity at the cost of availability: it's CP, and it blocks.
  • Saga = a chain of local transactions + compensations. Accept intermediate states ("pending") and design them in.
  • TCC is a saga where the first step is an explicit reservation that expires if not confirmed.
How to implement it
Library / APIPackageUse it for
TransactionScopebuilt-in System.TransactionsAn ambient transaction around your DB work. Escalation to a distributed (MSDTC) transaction works on Windows only since .NET 7 and needs TransactionManager.ImplicitDistributedTransactions = true
Orchestrated sagasMicrosoft.Azure.Functions.Worker.Extensions.DurableTask, TemporalioSaga steps and compensations as durable workflow code (see 11.6)
OutboxWolverineFx.EntityFrameworkCore or a hand-rolled outbox tableCommit the DB change and the outgoing event together, no 2PC needed (see 10.5)
// Works anywhere: one database, one local transaction
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    await accounts.DebitAsync(from, amount, ct);
    await ledger.PostAsync(from, to, amount, ct);   // same DB / connection string
    scope.Complete();
}

// Two different databases → escalates to MSDTC (Windows only). Avoid in new designs.
if (OperatingSystem.IsWindows())
    TransactionManager.ImplicitDistributedTransactions = true;

MSDTC escalation doesn't work in Linux containers, and it ties you to Windows hosts. In the cloud, design for sagas + outbox from day one.

Why don't microservices use distributed transactions? One word: blocking 🔒

1️⃣ Phase 1: every database prepares and holds its locks.

2️⃣ Phase 2: the coordinator says commit.

💀 Coordinator dies in between? Everyone waits, rows locked.

🔁 The alternative: a saga. Local transactions plus compensating "undo" steps.

💳 Card payments already work this way: authorise (hold), then capture or void.

Atomic and blocking, or available and eventually consistent. Which would you pick for payments?

#Microservices #DistributedSystems #Saga #SystemDesign

4.7 Time & ordering: clocks, Lamport, vector clocks Added

Every machine's clock is slightly wrong, so timestamps can't reliably order events across machines. Logical clocks order events by cause and effect instead.

🕰 Clock skew

Two servers' clocks disagree, often by milliseconds, sometimes by seconds after a bad NTP sync or VM pause.

⏪ Clocks jump

NTP can step a clock backwards. Leap seconds and VM migrations add surprises. "Later" can get a smaller timestamp.

⏱ Two kinds of clock

Wall clock (UTC) for display and logs. Monotonic clock for durations and timeouts: it never goes backwards.

⚠️ Last-write-wins (LWW) loses data silently

Two replicas accept updates to the same address. Node B's clock runs 2 s fast, so its older update gets the bigger timestamp and wins. The newer change disappears without any error. Cassandra, and Cosmos DB multi-region writes, resolve conflicts with LWW by default, so use it only where losing a concurrent update is acceptable.

🔢 Lamport timestamps

One counter per node. Tick on every event; on receive, take max(local, received) + 1. Gives a total order consistent with causality (break ties by node ID). Can't tell whether two events were concurrent.

🧮 Vector clocks

Each node keeps a counter for every node. Compare vectors to see "happened before" or concurrent (a real conflict). Used in Amazon's Dynamo paper and Riak. Size grows with the number of nodes.

🔀 Hybrid logical clocks (HLC)

Physical time plus a small logical counter. Stays close to wall-clock time but still respects causality. Used by CockroachDB, YugabyteDB and MongoDB cluster time.

🛰 TrueTime (Spanner)

GPS and atomic clocks give a time interval with bounded uncertainty; Spanner waits out the uncertainty before committing, so timestamps order transactions globally.

CRDTs: merge without conflicts

  • Never order cross-machine events by wall-clock timestamps alone. Use a sequence from one leader, a version number, or a logical clock.
  • Measure durations and timeouts with a monotonic clock (Stopwatch), not DateTime.Now.
  • Store and send times as UTC with an offset (DateTimeOffset). Keep NTP healthy and alert on skew.
How to implement it
Library / APIPackageUse it for
TimeProviderbuilt-in System.TimeProvider (.NET 8+)Inject time: GetUtcNow() for wall time, GetTimestamp() / GetElapsedTime() for durations, timers that tests can control
Fake time in testsMicrosoft.Extensions.TimeProvider.TestingFakeTimeProvider: move time forward to test TTLs, leases and timeouts without waiting
Monotonic durationsbuilt-in Stopwatch.GetTimestamp() / Stopwatch.GetElapsedTime()Measure elapsed time; unaffected by clock changes
UTC timestampsbuilt-in DateTimeOffset.UtcNowUnambiguous times for logs, events and audit records
// A thread-safe Lamport clock
public sealed class LamportClock
{
    private long _time;

    // Local event or just before sending a message
    public long Tick() => Interlocked.Increment(ref _time);

    // On receiving a message stamped with the sender's time
    public long Receive(long remote)
    {
        while (true)
        {
            long current = Interlocked.Read(ref _time);
            long next = Math.Max(current, remote) + 1;
            if (Interlocked.CompareExchange(ref _time, next, current) == current)
                return next;
        }
    }
}

Your servers don't agree on what time it is. And that can silently delete data 🕰️

⏩ Clocks drift and NTP can jump them backwards.

🗑 Last-write-wins + a fast clock = an older update beats a newer one, with no error.

🔢 Lamport clocks order events by cause and effect, not by wall time.

🧮 Vector clocks go further and detect true conflicts.

⏱ For timeouts, use a monotonic clock, never DateTime.Now.

Have you ever debugged a bug caused by clock skew?

#DistributedSystems #SystemDesign #SoftwareEngineering #DotNet

⚡ Quick-fire Q&A

How many nodes should a consensus cluster (etcd, ZooKeeper) have?

An odd number, usually 3 or 5. 2f + 1 nodes survive f failures: 3 survive 1, 5 survive 2. Adding a 4th node adds cost without adding fault tolerance.

Does R + W > N guarantee strong consistency?

It guarantees that read and write sets overlap, so a read normally sees the latest acknowledged write. Sloppy quorums, concurrent writes and partially failed writes can still give anomalies, so it's not full linearizability.

Is a Redis SET NX lock safe?

For efficiency (avoid duplicate work), yes, with a TTL and a release that checks the value. For correctness, no: a paused holder can outlive its lease. Add a fencing token checked by the storage, or make the operation idempotent.

Why not use 2PC across microservices?

It blocks when the coordinator fails, holds locks across services, needs every participant to support XA, and lowers availability. Use a saga with compensations plus the transactional outbox.

Why can't you order events from different servers by timestamp?

Clocks skew and can jump backwards, so a later event can get an earlier timestamp. Use a single sequencer, version numbers, or logical clocks (Lamport, vector, HLC).

← 03 · Scalability & Reliability