CAP Theorem & Caching
Why distributed systems can't have correctness, uptime, and partition tolerance all at once — and the practical CP vs AP choice that follows. Covers replication modes (async, sync, semi-sync), caching strategies (cache-aside, write-through, write-back), eviction policies, TTL, and Redis vs Memcached.
Assumes you know
After this you should be able to
- Explain the core idea in CAP Theorem & Caching
- Apply CAP Theorem & Caching in a representative design
System Design · Distributed Data
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.
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#
- CAP → pick 2 of 3: Consistency, Availability, Partition-tolerance.
- C = every read → latest data, same on all servers. (bank balance)
- A = every request → a response, may be stale. (Instagram feed)
- P = system keeps working despite network break between servers.
- Network partition is not optional in a distributed system → real choice = C vs A. doodletriangle C–A–P, one vertex crossed out
- CP → reject requests until consistent. Use: banking, payments, inventory.
- AP → serve stale, sync later. Use: social, streaming, DNS, Cassandra, Dynamo.
- CA → only single-server / standalone DB (no network → no partition). Not truly distributed; SPOF.
2 · Consistency Flavours#
| Type | Rule | Example |
|---|---|---|
| Strong | every read = latest | bank balance, payment |
| Eventual | replicas differ briefly, converge later | IG comments, reviews |
- Eventual consistency → lag = time to copy master → replica (e.g. ~2–3 min).
- Avoid single point of failure (SPOF) → replicate DB + backend services.
- Data lives in DB; services hold only app code → replica sync is a DB concern.
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) |
- Mechanism: master commits → writes to log → replicas tail log → replay. doodlemaster box → dotted "log" tape → 2 replica boxes reading it
- ⚠ Sync + replica down → wait / timeout → write fails entirely (not even master keeps it).
- ⭐ Semi-sync: primary crash → an ack'd replica can be promoted to master (no data loss).
- Locking (read/write, row-level) → done by DB, not app. ACID = DB's job.
4 · Caching#
- Cache = store hot data in RAM → skip repeat DB hits. Faster, lower latency, less DB load.
- Hit = in cache (fast). Miss = not there → hit DB, fill cache, return.
- Types: memory (Redis) · browser (CSS/JS/img) · DB query cache · CDN (static, edge-near user).
- Eviction (cache full): LRU (least recently used) · LFU (least frequently) · FIFO (oldest).
- Expiration: TTL → auto-remove after time. Low TTL = fresh + more DB hits; high TTL = stale risk.
- Invalidation: on DB write → delete or update matching cache entry.
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) |
- ⚠ Write-back + cache crash before flush → lose latest writes.
- ⭐ Cache-aside first request is always a miss (slow) → warm it if needed.
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 |
- Redis default for system design → in-memory speed + persistence covers RAM's weakness.
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.
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.
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.
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.
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.
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.
"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.
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.
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.
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.
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.
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.
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 |
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).
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.
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.
2.4.1Types of cache#
Client-side assets#
Images, CSS, JavaScript kept locally so revisits load without re-fetching.
Query results#
Stores results of frequently-run queries so the DB skips recomputing them.
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.
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.
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.
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.
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.
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 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 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.
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.
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.
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 |
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.
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#
- In a distributed system P is mandatory, so CAP is really a live C-vs-A choice made per partition.
- CP for correctness-critical data (banking), AP for uptime-critical data (social); CA only exists on a single server.
- Replication modes trade latency for durability: async (fast, lossy) → semi-sync (balanced, promotable) → sync (safe, slow, fails on a down replica).
- The commit log is the spine of all replication; locking and ACID are the database's job, not the app's.
- Caching trades a small stale-data risk for big wins in latency and DB load; manage it with eviction (LRU/LFU/FIFO), TTL, and invalidation.
- Strategy picks the read/write path: cache-aside (default), read-through, write-through (consistent), write-back (fast but crash-risky).
- Redis = RAM speed + disk persistence + rich features; the default cache over Memcached.
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#
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.
- Predict the outcome of a write when all replicas are healthy.
- Predict the exact outcome when one replica is down.
- State whether the goal is met, and if not, name the config that would meet it.
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 clientShow 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:
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 failureWhy. 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#
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.
- On a hit, return the cached value without touching the DB.
- On a miss, read the DB, store the result in the cache with a 300-second TTL, then return it.
- Expected: first call for an id is a miss (DB read + fill); the immediate second call is a hit (no DB read).
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.
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 rowWhy. 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.