System Design · Distributed Data

CAP, Replication and Caching

Once one machine is not enough, you copy your data across several. The moment you do, you inherit three hard questions — is every copy correct, is the system always answering, and does it survive a broken network? This note is how to reason about that trade, keep replicas in sync, and put a cache in front so the database stops drowning.

Updated August 2026 · Read ~22 min · Interview-focused


// the big idea

In any system where servers talk over a network, a break in that network forces a choice: keep answering with possibly-stale data (availability) or refuse to answer until every copy agrees (consistency). You cannot have both while staying partition-tolerant — so every replication and caching decision downstream is really you picking a side of that trade for your specific use case.

SECTION 1Notebook Summary

Hand-copy this. Dense and telegraphic; every line expands in the Detailed Notes below.

1 · CAP Theorem

2 · Consistency Flavours

Type Rule Example
Strong every read = latest bank balance, payment
Eventual replicas differ briefly, converge later IG comments, reviews

3 · Replication (master → replica sync)

Mode Master waits for Trade
Async nothing — replica reads logs later fast, risk data loss
Sync ALL replicas ack strong C, high latency
Semi-sync ≥1 replica ack balanced (common)

4 · Caching

5 · Caching Strategies

Strategy How Best for
Cache-aside app checks cache; miss → DB → fill read-heavy (default)
Read-through app talks to cache only; cache loads DB abstracted reads
Write-through write cache + DB together consistency
Write-back write cache now, DB later async write-heavy (risky)

6 · Redis vs Memcached

Feature Redis Memcached
Data types rich structures key–value only
Persistence yes (snapshot→disk) no
Scale / replication horizontal no
Also does sessions, rate-limit, pub/sub, queue simple cache

SECTION 2Detailed Study Notes

Picture a bank running on one server. It works — until that server dies, and every customer is locked out of their money at once. So you add a second server with a copy of the data, and a third. Now nobody is locked out by a single failure. But you have quietly created a new problem: you deposited ₹5,000 on server one, and a moment later someone checks their balance on server two, which has not heard the news yet. Which number is correct — and which do you show? Every idea in this note grows out of that one uncomfortable moment.

2.1The CAP Theorem

The CAP theorem says a distributed system can guarantee at most two of three properties at once: consistency, availability, and partition tolerance. "Distributed" just means your data lives on more than one machine that must talk over a network. Once that is true, CAP tells you that you cannot escape a trade-off — you only get to choose which pain you accept.

Let us define the three plainly before using the jargon.

C · consistency

Same answer, everywhere

Every read returns the newest data, no matter which server you hit. Transfer ₹5,000 out of ₹20,000 and every server must now say ₹15,000 — none may still say ₹20,000.

A · availability

Always an answer

Every request gets a response, even if the data is slightly old. Upload a photo and some followers see it instantly, others after a few seconds — but the app never stops responding.

P · partition tolerance

Survives a broken network

Servers keep working even when the network between them fails — an outage, a data-centre failure, a cloud networking issue. The system does not collapse.

Analogy · recall

Think of two shop tills that share one price book by walkie-talkie. Consistency is both tills always quoting the same price. Availability is each till always serving a customer. Partition tolerance is the walkie-talkie dying — and when it does, each till must choose: keep selling at a possibly-old price (A), or stop selling until the radio is back (C). It cannot do both.

Here is the part people miss. In a genuine distributed system, partitions are not optional — networks fail, and you must tolerate that. So P is effectively mandatory, and the real, live decision is always consistency versus availability. That reframing is the whole theorem.

Consistency Availability Partition tol. CP AP CA (single server only)
Each edge is an achievable pair. In a distributed system, P is forced — so you travel the C↔A base.

2.1.1The three CAP choices in practice

CP — choose correctness

  • During a partition, the system rejects requests until data is consistent again.
  • Rejecting = temporarily not available.
  • Use when wrong data is unacceptable: banking, payments, inventory.

AP — choose uptime

  • During a partition, keep serving — even if the answer is stale.
  • Replicas reconcile afterwards (eventual consistency).
  • Use when uptime beats freshness: social, streaming, DNS, Cassandra, DynamoDB.

The third pairing, CA, only exists where there is no network to partition — a single server with a standalone database. It is consistent and available because there is nothing to fall out of sync. But it is not really a distributed system, and it carries a single point of failure: if that one machine dies, everything dies. It also cannot scale out. So in interviews, CA is the "technically true, practically irrelevant" corner.

⚠ PITFALL

"Rejecting a request until consistent" is exactly what makes a CP system unavailable in that window — people wrongly assume consistency and availability are independent knobs. They are not: under a partition, buying more of one spends the other. Say this trade out loud in an interview; it is the point of the whole theorem.

💡 TIP

Interviewers rarely ask "what is CAP?" directly. They ask you to design something, then probe your database and consistency choices. Naming your CAP stance unprompted — "this is payments, so I'll take CP and accept rejecting writes during a partition" — signals seniority. Always justify the pick against the requirement; a contradictory justification (choosing availability for a bank) is the tell of a weak answer.

2.2Strong vs Eventual Consistency

Strong consistency means every read returns the latest write; eventual consistency means replicas may briefly disagree, then converge. The word "eventually" is doing real work — it names the gap between "master updated" and "all replicas caught up".

Go back to the two-server bank. You write ₹50 to a variable on the master (it was ₹40). The master now says 50. The replica still says 40 until the change is copied over. If a read lands on the replica during that window, it sees 40 — stale. A CP system refuses that read until the replica catches up; an AP system serves the 40 and lets you catch up later. That copy lag — maybe two or three minutes in a bad case — is the "eventual" in eventual consistency.

Analogy · recall

Eventual consistency is a group chat where one person's message is still "sending". Everyone will see it soon; right now, different phones show different states. Nobody is broken — the system just has not finished propagating.

Property Strong Eventual
Read guarantee Always latest value Maybe stale, converges later
CAP leaning CP AP
Cost Higher latency Temporary staleness
Examples Bank balance, online payment Instagram comments, product reviews

One clarification that resolves a lot of confusion: what actually needs replicating is the data, i.e. the database. Your backend services are just copies of the same application code — deploy the same container image to four servers and they are interchangeable, holding no unique state. The database is the shared source of truth, so keeping replicas in agreement is a database responsibility, not something your application logic handles. [+] This is also why locking (below) lives in the DB.

2.3Data Replication

Replication is keeping copies of your database (and services) on multiple machines so no single failure takes you down. You designate one copy as the master (or primary), which takes writes, and one or more replicas (or slaves) that mirror it. The interesting question is how the master tells the replicas about a new write — and how long it waits for them.

The core mechanism is the same across modes: when the master commits a write, it records that committed transaction in a log. Replicas continuously tail that log, spot the new commit, and replay the identical operation on themselves. A commit cannot be rolled back, so replaying it is safe. The three modes differ only in when the master considers the write "done" for the client.

Master (primary) commit log append-only Replica 1 Replica 2 write tail + replay
Every mode shares this spine: commit → log → replicas replay. Modes differ only in how long the master waits.

2.3.1Asynchronous replication

The master commits and replies to the client immediately, without waiting for any replica. Replicas catch up later by reading the log. Fast for the client, but if the master dies before a replica has caught up, those writes are lost. This is the common default.

client write→ master commits→ reply to client→ replicas catch up later

2.3.2Synchronous replication

The master writes locally but does not commit or reply yet. It pushes the write to the replicas and waits for all of them to acknowledge they have saved it. Only then does the master finalise its own commit and return success to the client. This gives strong consistency — the client is not told "done" until every copy agrees — at the cost of latency, since the slowest replica sets the pace.

client write→ master writes locally→ all replicas ack→ master commits→ reply success
⚠ PITFALL

If a replica is down under synchronous replication, the master waits for an acknowledgement that never comes. After a configured timeout it fails the whole write — the ₹5,000 is not saved even on the master. Beginners assume "at least the master keeps it"; it does not, because sync mode's promise is all-or-nothing across every replica.

2.3.3Semi-synchronous replication

The middle path, and the one most real systems pick. The master writes locally and pushes to all replicas, but waits for only at least one acknowledgement before confirming success to the client. The remaining replicas sync asynchronously afterwards. This cuts the risk of data loss (the write survives on at least two machines) without paying the full latency of waiting for everyone.

⭐ TRICK

The quiet payoff of semi-sync: because at least one replica is guaranteed to have the write, if the master crashes you can promote that replica to master with zero data loss. That is why semi-sync is the sensible default — you get most of sync's safety and most of async's speed, plus a clean failover story. Reach for it when someone asks "what if the primary dies?"

Mode Master waits for Consistency Latency Data-loss risk
Async Nothing Eventual Lowest Highest
Sync All replicas Strong Highest Lowest
Semi-sync ≥ 1 replica Near-strong Medium Low
⭐ TRICK

All three modes answer one question: how many copies must confirm before I say "done"? None = async, one = semi-sync, all = sync. Memorise the question and you can re-derive the whole table on the spot. And note there is no "perfect" mode — every option is a trade-off, which is the entire spirit of distributed systems. Interviewers aren't after the "right" answer; they want you to justify the pick against the system's needs (a payment flow leans sync or semi-sync; a like-counter is happy with async).

// why it matters

All of this — the read locks and write locks that make it safe, the row-level locking that keeps performance high — happens inside the database, not your application. The DB owns the ACID properties (atomicity, consistency, isolation, durability); your service just reads and writes and lets the DB decide which locks to take. A common interview stumble is claiming you manage locks in application code. You do not.

2.4Caching

A cache stores frequently used data in fast memory (RAM) so repeated requests skip the slow trip to the database. Imagine 10,000 people opening the same product page in a minute. Without a cache, that is 10,000 identical read queries hammering your database. With one, the first request fills the cache and the other 9,999 are served straight from memory — the database sees roughly one query.

Analogy · recall

A cache is the notepad on your desk versus the filing cabinet down the hall. The first time you need a figure you walk to the cabinet (the database) and copy it onto the notepad (the cache). Every time after, you just glance at the notepad. The trade is obvious: the notepad is tiny, and if the figure changes in the cabinet, your note goes stale.

The read path has two outcomes. A cache hit means the data was in the cache — returned fast, straight from RAM. A cache miss means it was not: the app reads the database, stores the result in the cache, and returns it, so the next identical request will hit. The payoffs are faster responses, lower latency, less database load, and better scalability.

request→ cache?→ hit → return (fast)
request→ cache?→ miss → read DB→ fill cache→ return

2.4.1Types of cache

memory cache

In-RAM store

A dedicated fast store like Redis. The workhorse of backend caching.

browser cache

Client-side assets

Images, CSS, JavaScript kept locally so revisits load without re-fetching.

database cache

Query results

Stores results of frequently-run queries so the DB skips recomputing them.

CDN cache

Edge, near the user

Static content (images, video, CSS, JS) served from the server physically closest to the client, cutting network latency. e.g. Cloudflare, Akamai, CloudFront.

💡 TIP

What a cache stores is flexible — most commonly serialized objects, but also JSON strings, images, whole HTML pages, or full API responses. In an interview, don't over-claim ("a cache always stores objects"); say it holds whatever representation lets you skip the expensive work, usually a serialized object.

What you cache matters as much as how. Caching pays off best on mostly-static data — profiles, product information, configuration — where a value stays valid long enough to be reused. For data that changes constantly you don't abandon the cache, you shorten the TTL and invalidate on write.

2.4.2Eviction, expiration, invalidation

RAM is small, so a cache cannot keep everything. Three mechanisms manage that scarce space.

Eviction decides what to drop when the cache is full. LRU (least recently used) drops whatever has not been touched in the longest time — it keeps hot data around. LFU (least frequently used) drops whatever has been accessed the fewest times. FIFO drops the oldest entry regardless of use — simple, but weaker than LRU.

LRU least recently used now … old ✕ drop the stalest ★ the usual default LFU least frequently used ×9 ×5 ×1 ✕ drop the rarest-hit FIFO first in, first out 1st ✕ 2nd 3rd drop the oldest arrival ignores actual use
The highlighted box is the victim under each policy: LRU tracks recency, LFU tracks frequency, FIFO tracks arrival order.

Expiration (TTL, time-to-live) stamps each entry with a lifespan. A user profile might live 10 minutes, a product list 30. When the TTL expires, the entry is removed and the next request refetches from the database. TTL stops the cache serving something forever-stale.

Invalidation handles the case where the underlying data changes before its TTL is up: when you write to the database, you also delete or update the matching cache entry so nobody reads the old value.

⚠ PITFALL

TTL is a two-sided trade, and interviewers love pushing on it. A short TTL means fresher data but more database hits (you refetch often). A long TTL means fewer hits but a bigger window to serve stale data after a write. There is no "correct" TTL — you tune it to how tolerant the data is of staleness, and you should say so rather than picking a number blindly.

2.4.3Caching strategies

These describe who talks to what, and in what order on reads and writes.

cache-aside · lazy load

App checks cache first

App looks in cache; on a miss it reads the DB, fills the cache, returns. Simple, memory-efficient. Downside: the first request for any key is always a slow miss.

read-through

Cache fronts the DB

App only ever talks to the cache. On a miss, the cache itself loads from the DB and returns. The app never touches the database directly.

write-through

Write both, together

Every write goes to cache and DB at the same time. Great for consistency — the cache is never stale. Downside: writes are slower (two stores per write).

write-back · write-behind

Write cache now, DB later

Write to cache immediately, flush to DB asynchronously. Fast for write-heavy loads. Downside: if the cache crashes before the flush, the newest writes are gone.

write-through write cache DB both at once write-back write cache DB later
Write-through pays latency for a never-stale cache; write-back pays a crash-window risk for write speed.

2.5Redis, and why it beats Memcached

Redis is an in-memory database, most often used as the cache in system design. Because it keeps data in RAM, reads and writes are extremely fast — far faster than a disk-based store. RAM's weakness is that it is volatile: lose power and you lose the data. Redis answers that with persistence — periodically it takes a snapshot of its in-memory state and writes it to disk, so after a crash or restart it can reload. You get RAM speed and disk-backed recovery.

Analogy · recall

Redis persistence is autosave in a document editor. You work at memory speed on the live document; every few minutes it quietly saves a copy to disk. Crash, and you reopen from the last save — you lose seconds, not the whole document.

client requests Redis · in RAM fast, but volatile periodic snapshot Disk durable, survives restart crash → reload from the last snapshot
Reads are answered from RAM for speed; a periodic snapshot to disk is what makes that speed survivable.

Beyond caching, Redis is horizontally scalable (run many instances across servers), supports rich data structures rather than plain key–value, and doubles as session storage, a rate limiter, a message queue, and a publish/subscribe bus. Memcached, by contrast, is a lightweight key–value cache: simpler and fine for basic lookups, but no persistence, no replication, no rich types.

Feature Redis Memcached
Data types Rich structures Key–value only
Persistence Yes (snapshot to disk) No
Replication / scale Horizontal No
Transactions Yes No
Pub/sub Yes No
Memory footprint Higher Lower
💡 TIP

The instructor's scripted answer for interviews: name Memcached first, grant it one or two honest merits (simpler, lighter), then pivot — "but I'd choose Redis for its persistence, horizontal scalability, and richer data types." Showing you know the alternative and then justifying your pick reads far stronger than only ever naming Redis.

💡 TIP · the meta-move

The second habit from the session, and the more valuable one: drop concept names on purpose to bait the interviewer's next question. Say "I'd put a cache here to keep read latency low" and you have planted a thread — the natural follow-up is about eviction, TTL, or Redis, all ground you have prepared. It works like mentioning you love cricket so people ask you about cricket. You are not dodging the interview; you are steering it toward your strengths.

2.6Interview & Practice Q&A

Q1Why can't a distributed system have all three of C, A, and P?

Because a network partition will eventually happen, so P is forced on you. When the network splits, servers can't sync, and you must choose per request: answer with possibly-stale data (sacrifice C) or refuse to answer until data agrees (sacrifice A). You can't do both simultaneously during the split, so at most two of the three hold.

Q2Give a CP and an AP example and justify each.

CP: a banking transfer — showing a wrong balance is unacceptable, so during a partition you'd rather reject the request than return stale money. AP: an Instagram feed — a follower seeing a photo a few seconds late is fine, and the app going down is not, so you serve stale and reconcile later. The rule is: does wrong-but-fast beat right-but-refused for this data?

Q3What is eventual consistency, and where's the "eventual" gap?

It means replicas can hold different values briefly, then converge to the same latest value. The gap is the replication lag — the time to copy the committed change from master to replica. A read during that window may return stale data; a read after it returns the latest. It's the model behind most AP systems.

Q4Compare async, sync, and semi-sync replication.

Async: master replies immediately, replicas catch up via the log later — fastest, but writes can be lost if the master dies first. Sync: master waits for all replicas to ack before committing — strong consistency, highest latency, and a down replica fails the write. Semi-sync: master waits for at least one ack — balanced risk and latency, and the ack'd replica can be promoted on failover.

Q5Under synchronous replication, what happens if a replica is down?

The master waits for an acknowledgement it will never receive. After a configured timeout it fails the entire write and returns an error to the client — the value is not saved even on the master, because sync mode requires all replicas to confirm. That's the availability cost of strong consistency.

Q6Where does locking happen — application or database?

The database. It manages read locks, write locks, and fine-grained row-level locks as part of enforcing ACID. Row-level locking (rather than locking the whole table) is what keeps performance up: only the row being written is locked, so other rows stay readable and writable. Your application just issues reads and writes and never manages locks itself.

Q7When would you pick write-back over write-through?

Write-back suits write-heavy workloads where you can't afford to pay a DB write on every request — you write to cache instantly and flush to the DB asynchronously. The cost is durability: if the cache crashes before flushing, the newest writes are lost. Write-through is the opposite choice — slower writes, but the cache and DB never disagree, so pick it when consistency matters more than write throughput.

Q8How does TTL trade off freshness against load?

A short TTL expires entries quickly, so you refetch from the DB often — fresher data, more database load. A long TTL keeps entries around — fewer DB hits, but a wider window in which a post-write change isn't reflected, so more stale reads. You tune TTL to how stale the data is allowed to be, and pair it with invalidation-on-write for data that changes unpredictably.

Q9Why is Redis usually preferred over Memcached in system design?

Redis keeps RAM-speed reads/writes but adds persistence via periodic disk snapshots, so a crash or restart doesn't wipe the data. It's also horizontally scalable, supports rich data types and transactions, and can serve as a session store, rate limiter, and pub/sub bus. Memcached is simpler and lighter but is key–value only with no persistence or replication — fine for basic caching, weaker as a general building block.

Q10How does a replica know it is holding stale data and needs to sync?

The master records every committed transaction in an append-only log. Each replica continuously tails that log, spots commits it has not applied, and replays the identical operations against its own copy. Nothing in the application is involved — the replica is not "told" it is stale so much as it is always reading forward from wherever it got to. This is why the mechanism is the same for async, sync, and semi-sync: only the master's willingness to wait changes.

2.7Key Takeaways

pick 2 of 3 P is forced → C vs A CP = banking AP = social eventual = lag commit → log → replay sync waits for all semi-sync = promotable locks live in the DB hit vs miss LRU / LFU / FIFO TTL trade-off write-back = crash risk Redis persists

SECTION 3Microexercises

Reading proves you recognise these ideas. These prove you can use them. Attempt each before opening the walkthrough.

3.1Exercise 1 — Diagnose the replication config

// your task

A payments team wants a write to be lost never, even if the primary machine dies right after committing. They've configured replication as below. Predict what happens on a normal write, then on a write where one replica is temporarily down, and say whether this config actually meets their "never lose a committed write" goal.

replication.yaml
replication:
  mode: synchronous        # master waits for replica acknowledgements
  wait_for: all            # every replica must ack before commit
  ack_timeout_seconds: 30  # then give up
  on_timeout: fail_write   # return an error to the client
Show the full walkthrough

Step 1 — Trace the healthy path. In synchronous mode the master writes locally but does not commit yet. It pushes the write to all replicas and waits. When every replica acknowledges, the master finalises its commit and returns success. So a healthy write succeeds — with latency set by the slowest replica.

Step 2 — Trace the degraded path. With one replica down, its acknowledgement never arrives. Because wait_for: all, the master keeps waiting until ack_timeout_seconds elapses, then on_timeout: fail_write triggers. The write fails entirely — it is not even kept on the master. The client gets an error.

Root cause / verdict. The goal is "never lose a committed write." This config never loses a committed write — but it achieves that partly by refusing to commit at all whenever any replica is down, which is a severe availability hit. Worse, requiring all replicas makes every replica a single point of failure for writes. The safer fit is semi-synchronous: wait for at least one replica ack, so the write survives on ≥2 machines and an ack'd replica can be promoted if the master dies — without one sick replica halting all writes.

replication.yaml — corrected
replication:
  mode: semi_synchronous   # wait for at least one replica
  wait_for: 1              # >= 1 ack, not all
  ack_timeout_seconds: 30
  on_timeout: fail_write   # still fail rather than lose durability
# remaining replicas sync asynchronously afterwards;
# any ack'd replica is promotable on primary failure

Why. Sync's "all replicas" promise buys strong consistency at the price of availability and a fragile write path; semi-sync keeps the no-data-loss + promotable-failover guarantee the payments team actually needs, without letting one down replica block every write.

3.2Exercise 2 — Build a cache-aside read

// your task

Write a cache-aside read function get_product(id) against an in-memory store (a Redis stand-in) and a database. Follow the cache-aside contract exactly.

starter — cache and db are given
cache = {}   # stand-in for Redis: key -> value (TTL handled by set_with_ttl)
db = {"p1": {"name": "Desk Lamp", "price": 1200}}

def set_with_ttl(key, value, ttl):
    cache[key] = value   # assume TTL expiry is handled for you

def db_read(id):
    return db.get(id)    # returns the row dict, or None

def get_product(id):
    # TODO: implement cache-aside
    ...
Show the full walkthrough

Step 1 — Check the cache first. Cache-aside means the application owns the lookup order. Look for the key in the cache. If it's present, that's a hit — return it immediately and never call db_read.

Step 2 — Handle the miss. If the key isn't cached, read the database. If the DB has no row, return None (don't cache a miss here). Otherwise store the row with the required 300s TTL, then return it — so the next identical call hits.

Step 3 — Verify the contract. First get_product("p1"): not in cache → DB read → fill → return. Second call: found in cache → return with no DB read. That matches the expected hit/miss pattern.

solution.py
def get_product(id):
    if id in cache:              # cache hit
        return cache[id]
    row = db_read(id)            # cache miss -> go to DB
    if row is None:
        return None
    set_with_ttl(id, row, 300)   # fill cache for next time
    return row

Why. The app checking the cache before the DB, and populating only on a miss, is cache-aside (lazy loading). The 300s TTL is your staleness bound; pair it with invalidation on write if p1's price can change before the TTL expires.

sync + down replica = failed write semi-sync = no loss + promotable check cache → miss → DB → fill never cache before the DB TTL = staleness bound