Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design)  Module 7 – Caching

Cache-aside, read-through, write-through, write-back

How cache-aside, read-through, write-through and write-back move data between a cache and its database, compared side by side, with cache-aside in code.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 8 of 25 minutes free
Free: A strategy answers two questions: who fills a miss, and what a write does · The four strategies differ in who loads a miss and when a write returns · Cache-aside in code: write the database, then delete the copy · Practise: write get and put for a cache-aside store. With a pass: 2 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Implement cache-aside with a TTL and a delete after every write
  • Compare read-through, write-through, write-back and refresh-ahead by who fills the cache and what is at risk
  • Work out the writes a write-back cache can lose and the hit ratio a database needs
  • Choose a strategy from a workload's freshness and durability needs

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement cache-aside with a TTL and a delete after every write
  • Compare read-through, write-through, write-back and refresh-ahead by who fills the cache and what is at risk
  • Work out the writes a write-back cache can lose and the hit ratio a database needs
  • Choose a strategy from a workload's freshness and durability needs

Before you start

On this page

The four names say who copies data between a cache and its database, and when. With cache-aside, the application reads the cache, and on a miss it reads the database and stores the result in the cache; after a write it updates the database and deletes the cached copy. With read-through, the cache loads a missing value from the database by itself. With write-through, a write goes to the cache and the database before it is acknowledged. With write-back, also called write-behind, the cache acknowledges a write at once and copies it to the database later. Two of them shape the read path and two the write path, so real systems combine them. Choose with two questions: how fresh must a read be after a write, and how much data may you lose if the cache fails?

A strategy answers two questions: who fills a miss, and what a write does

A cache only helps while it is both useful and correct. It is useful when reads find what they need in it, which depends on who fills it after a miss and how soon. It is correct when the copy it holds still matches the data, which depends on what happens to that copy after every write. A strategy is an answer to both: a rule for the read path and a rule for the write path.

The answers have costs that show up elsewhere. A strategy that fills the cache on writes makes writes slower. One that delays writes to the database makes them fast and puts acknowledged data at risk. One that keeps the logic in the application gives the application a way around a failed cache; one that hides the database behind the cache does not. Each panel below is one strategy; follow its arrows to see who reads and writes the database:

Four panels: cache-aside, read-through, write-through and write-back, each with an app, a cache and a database.Cache-asideRead-throughWrite-throughWrite-backAppCacheDatabaseAppCacheDatabaseAppCacheDatabaseAppCacheDatabaseget; set aftera miss; deleteafter a writeread on a miss;every writeevery readloads a missevery writewrites itbefore the ackevery write,acked at oncewrites later,in batches

The four strategies: who talks to the database

Text description of the diagram

Four panels stand in two rows of two. Each has the same three boxes from top to bottom: the app, the cache and the database.

  1. Cache-aside: the app talks to both. It gets from the cache, reads the database on a miss and then sets the value in the cache. Every write goes from the app to the database, and the app then deletes the key from the cache.
  2. Read-through: the app sends every read to the cache, and the cache itself loads a missing value from the database.
  3. Write-through: the app sends every write to the cache, and the cache writes it to the database before it acknowledges the write.
  4. Write-back: the app sends every write to the cache, which acknowledges it at once and writes it to the database later, in batches.

Checkpoint

Check: Two questions every strategy answers

  1. Question 1 of 2 Which two questions does a caching strategy answer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Who fills the cache after a miss, and what happens to the cached copy after a write

    A cache helps only while it is useful and correct. It is useful when reads find what they need, which depends on who fills it after a miss; it is correct when its copies match the data, which depends on what each write does to them. A strategy is a rule for each path.

  2. Question 2 of 2 In which strategies of the diagram does every read or write it shows pass through the cache on the way to the database? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Write-back
    • Write-through
    • Read-through

    Read-through, write-through and write-back put the cache in the data path, which gives every caller the same behaviour and no way around a failed cache. Cache-aside keeps the logic in the application, which talks to both.

The four strategies differ in who loads a miss and when a write returns

Cache-aside keeps all the logic in the application, which talks to the cache and the database separately. It is the usual default because a failed cache only makes the application slower: it can still read the database. The Azure cache-aside pattern also notes that it does not guarantee consistency, because anything that changes the database without deleting the cached copy leaves that copy stale.

Read-through moves the loading into the cache layer, often a library or a caching product, so every caller gets the same behaviour without writing it. A read-through cache needs its own access to the database, and when the cache is down the callers have no other path to the data.

Write-through sends each write through the cache, which stores it in the database before the write returns. Reads right after a write find the new value, and a write-through write takes as long as both writes together: the Oracle Coherence documentation says plainly that it does not improve write performance at all.

Write-back acknowledges a write as soon as the cache has it and copies it to the database after a delay, combining several changes to one key into one database write. Writes are fast, and the cache becomes the only holder of recent writes until they are copied, so a cache failure can lose them.

The read path: cache-aside or read-through
Criterion Cache-asideRead-through
Who loads a miss The applicationThe cache
Where the logic lives In every callerIn one cache layer
Writes Database, then delete the copyUsually the same
Cache down Reads go to the databaseReads fail
When to choose Read-heavy data, seconds of staleness allowedMany services that share one loading rule
The write path: write-through or write-back
Criterion Write-throughWrite-back
Write returns After the database has itWhen the cache has it
Reads just after a write The new valueThe new value, from the cache
Lost if the cache dies NothingWrites not yet copied
Cache down Writes failReads and writes fail
When to choose Reads that must see the last write; moderate write ratesWrite rates the database cannot take; small losses allowed

A fifth name, refresh-ahead, describes a cache that reloads popular entries shortly before they expire, so readers rarely wait for a miss. It costs database reads for entries nobody reads again.

Checkpoint

Check: The four strategies compared

  1. Question 1 of 2 Which strategy can lose writes that the application was already told had succeeded, if the cache fails?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Write-back, because it acknowledges a write before the database has it

    Write-back stores the write in the cache, acknowledges it and copies it to the database later. Until that copy is made, the cache holds the only copy. Write-through acknowledges only after the database has stored the write, and cache-aside writes the database first.

  2. Question 2 of 2 The cache cluster is down for five minutes. Under which strategy do reads keep working, more slowly?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Cache-aside

    Cache-aside is the usual default because a failed cache only makes the application slower: it can still read the database. The other three hide the database behind the cache.

Cache-aside in code: write the database, then delete the copy

The two rules of cache-aside fit in a few lines. This program reads prices through a cache with a 60-second TTL (cache entry): Time to live: how long a cache may serve an entry after storing it. When it runs out the next read is a miss, so even a copy that a missed delete left behind disappears., changes one price, and uses a fake clock so that every run prints the same trace:

One program · 14 languages Cache-aside with a TTL and a delete after each write

Python 3.14 · main.py

TTL_MS = 60_000
database = {"p1": 499}  # product id -> price
cache: dict[str, tuple[int, int]] = {}  # product id -> (price, expires at)


def get_price(product: str, now: int) -> tuple[str, str]:
    entry = cache.get(product)
    if entry is not None and entry[1] > now:
        return str(entry[0]), "hit"
    price = database.get(product)
    if price is None:
        return "none", "miss: no such product, nothing cached"
    cache[product] = (price, now + TTL_MS)
    if entry is not None:
        return str(price), "miss: expired, read again and cached"
    return str(price), "miss: read the database, cached for 60 s"


def set_price(product: str, price: int) -> tuple[str, str]:
    database[product] = price  # 1. the database first: the source of truth
    cache.pop(product, None)  # 2. then delete the copy: the next read reloads
    return "", "database updated, then the copy deleted"


steps = [
    (0, "p1", None),
    (10, "p1", None),
    (20, "p9", None),
    (30, "p1", 449),
    (40, "p1", None),
    (50, "p1", None),
    (60_100, "p1", None),
]
for ms, product, new_price in steps:
    if new_price is None:
        label, (value, how) = f"get {product}", get_price(product, ms)
    else:
        label, (value, how) = (
            f"set {product} to {new_price}",
            set_price(product, new_price),
        )
    print(f"{ms:>6} ms  {label:<14} {value:>4}  {how}")

In Python: cache.pop(product, None) deletes the cached copy and does nothing when there is none, so a write never fails on a key that was never cached.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const TTL_MS = 60_000;
const database = new Map([["p1", 499]]); // product id -> price
const cache = new Map(); // product id -> { price, expiresAt }

function getPrice(product, now) {
  const entry = cache.get(product);
  if (entry !== undefined && entry.expiresAt > now) return [String(entry.price), "hit"];
  const price = database.get(product);
  if (price === undefined) return ["none", "miss: no such product, nothing cached"];
  cache.set(product, { price, expiresAt: now + TTL_MS });
  if (entry !== undefined) {
    return [String(price), "miss: expired, read again and cached"];
  }
  return [String(price), "miss: read the database, cached for 60 s"];
}

function setPrice(product, price) {
  database.set(product, price); // 1. the database first: the source of truth
  cache.delete(product); // 2. then delete the copy: the next read reloads
  return ["", "database updated, then the copy deleted"];
}

const steps = [
  [0, "p1"],
  [10, "p1"],
  [20, "p9"],
  [30, "p1", 449],
  [40, "p1"],
  [50, "p1"],
  [60_100, "p1"],
];
for (const [ms, product, newPrice] of steps) {
  const write = newPrice !== undefined;
  const label = write ? `set ${product} to ${newPrice}` : `get ${product}`;
  const [value, how] = write ? setPrice(product, newPrice) : getPrice(product, ms);
  console.log(`${String(ms).padStart(6)} ms  ${label.padEnd(14)} ${value.padStart(4)}  ${how}`);
}

In JavaScript: A Map's get returns undefined for a missing key, which is how both the cache and the stand-in database say that they hold nothing.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class Main {
  static final int TTL_MS = 60_000;
  static final Map<String, Integer> database = new HashMap<>(Map.of("p1", 499)); // id -> price
  static final Map<String, Entry> cache = new HashMap<>(); // product id -> entry

  record Entry(int price, int expiresAt) {}

  record Step(int ms, String product, Integer newPrice) {}

  record Outcome(String value, String how) {}

  static Outcome getPrice(String product, int now) {
    Entry entry = cache.get(product);
    if (entry != null && entry.expiresAt() > now) {
      return new Outcome(String.valueOf(entry.price()), "hit");
    }
    Integer price = database.get(product);
    if (price == null) {
      return new Outcome("none", "miss: no such product, nothing cached");
    }
    cache.put(product, new Entry(price, now + TTL_MS));
    String how =
        entry != null
            ? "miss: expired, read again and cached"
            : "miss: read the database, cached for 60 s";
    return new Outcome(String.valueOf(price), how);
  }

  static Outcome setPrice(String product, int price) {
    database.put(product, price); // 1. the database first: the source of truth
    cache.remove(product); // 2. then delete the copy: the next read reloads
    return new Outcome("", "database updated, then the copy deleted");
  }

  public static void main(String[] args) {
    Locale.setDefault(Locale.ROOT);
    List<Step> steps =
        List.of(
            new Step(0, "p1", null),
            new Step(10, "p1", null),
            new Step(20, "p9", null),
            new Step(30, "p1", 449),
            new Step(40, "p1", null),
            new Step(50, "p1", null),
            new Step(60_100, "p1", null));
    for (Step step : steps) {
      String product = step.product();
      Integer price = step.newPrice();
      String label = price != null ? "set " + product + " to " + price : "get " + product;
      Outcome out = price != null ? setPrice(product, price) : getPrice(product, step.ms());
      System.out.printf("%6d ms  %-14s %4s  %s%n", step.ms(), label, out.value(), out.how());
    }
  }
}

In Java: Map.get returns null for a missing product, so the price is an Integer rather than an int; a step's newPrice is an Integer too, and null means a read.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <format>
#include <optional>
#include <print>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

constexpr int ttl_ms = 60'000;

struct Entry {
  int price;
  int expires_at;
};

// product id -> price, and product id -> entry
std::unordered_map<std::string, int> database{{"p1", 499}};
std::unordered_map<std::string, Entry> cache;

std::pair<std::string, std::string> get_price(const std::string &product,
                                              int now) {
  const auto cached = cache.find(product);
  if (cached != cache.end() && cached->second.expires_at > now) {
    return {std::to_string(cached->second.price), "hit"};
  }
  const bool expired = cached != cache.end();
  const auto row = database.find(product);
  if (row == database.end()) {
    return {"none", "miss: no such product, nothing cached"};
  }
  const int price = row->second;
  cache[product] = {price, now + ttl_ms};
  return {std::to_string(price),
          expired ? "miss: expired, read again and cached"
                  : "miss: read the database, cached for 60 s"};
}

std::pair<std::string, std::string> set_price(const std::string &product,
                                              int price) {
  database[product] = price; // 1. the database first: the source of truth
  cache.erase(product);      // 2. then delete the copy: the next read reloads
  return {"", "database updated, then the copy deleted"};
}

struct Step {
  int ms;
  std::string product;
  std::optional<int> new_price;
};

int main() {
  const std::vector<Step> steps = {
      {0, "p1", {}},  {10, "p1", {}}, {20, "p9", {}},     {30, "p1", 449},
      {40, "p1", {}}, {50, "p1", {}}, {60'100, "p1", {}},
  };
  for (const auto &[ms, product, new_price] : steps) {
    const std::string label =
        new_price ? std::format("set {} to {}", product, *new_price)
                  : "get " + product;
    const auto [value, how] =
        new_price ? set_price(product, *new_price) : get_price(product, ms);
    std::println("{:>6} ms  {:<14} {:>4}  {}", ms, label, value, how);
  }
}

In C++: The miss is noted as expired before cache[product] inserts the new entry, because an insertion may rehash the map and leave the earlier iterator invalid.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

const int TtlMs = 60_000;
var database = new Dictionary<string, int> { ["p1"] = 499 }; // product id -> price
var cache = new Dictionary<string, (int Price, int ExpiresAt)>(); // product id -> entry

(int Ms, string Product, int? NewPrice)[] steps =
[
    (0, "p1", null),
    (10, "p1", null),
    (20, "p9", null),
    (30, "p1", 449),
    (40, "p1", null),
    (50, "p1", null),
    (60_100, "p1", null),
];
foreach (var (ms, product, newPrice) in steps)
{
    var (label, (value, how)) = newPrice is int price
        ? ($"set {product} to {price}", SetPrice(product, price))
        : ($"get {product}", GetPrice(product, ms));
    Console.WriteLine($"{ms,6} ms  {label,-14} {value,4}  {how}");
}

(string, string) GetPrice(string product, int now)
{
    bool cached = cache.TryGetValue(product, out var entry);
    if (cached && entry.ExpiresAt > now)
    {
        return ($"{entry.Price}", "hit");
    }
    if (!database.TryGetValue(product, out int price))
    {
        return ("none", "miss: no such product, nothing cached");
    }
    cache[product] = (price, now + TtlMs);
    return cached
        ? ($"{price}", "miss: expired, read again and cached")
        : ($"{price}", "miss: read the database, cached for 60 s");
}

(string, string) SetPrice(string product, int price)
{
    database[product] = price; // 1. the database first: the source of truth
    cache.Remove(product); // 2. then delete the copy: the next read reloads
    return ("", "database updated, then the copy deleted");
}

In C#: TryGetValue reads a key and says whether it was there in one call, and newPrice is int price tells a write from a read and takes its value.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>
#include <string.h>

enum { TTL_MS = 60000 };

typedef struct {
  const char *product; // NULL when empty
  int price, expires_at;
} Row;

static Row database = {"p1", 499, 0}; // one product id -> price
static Row cache; // one slot: product id -> price and expiry

static Row *holding(Row *row, const char *product) {
  return row->product != NULL && strcmp(row->product, product) == 0 ? row
                                                                    : NULL;
}

static const char *get_price(const char *product, int now, char *value) {
  const Row *entry = holding(&cache, product);
  if (entry != NULL && entry->expires_at > now) {
    snprintf(value, 16, "%d", entry->price);
    return "hit";
  }
  const Row *row = holding(&database, product);
  if (row == NULL) {
    snprintf(value, 16, "none");
    return "miss: no such product, nothing cached";
  }
  cache = (Row){row->product, row->price, now + TTL_MS};
  snprintf(value, 16, "%d", row->price);
  return entry != NULL ? "miss: expired, read again and cached"
                       : "miss: read the database, cached for 60 s";
}

static const char *set_price(const char *product, int price) {
  Row *row = holding(&database, product);
  if (row != NULL) {
    row->price = price; // 1. the database first: the source of truth
  }
  if (holding(&cache, product) != NULL) {
    cache.product = NULL; // 2. then delete the copy: the next read reloads
  }
  return "database updated, then the copy deleted";
}

int main(void) {
  // A new price of 0 means a read.
  static const struct {
    int ms;
    const char *product;
    int new_price;
  } steps[] = {{0, "p1", 0},  {10, "p1", 0}, {20, "p9", 0},   {30, "p1", 449},
               {40, "p1", 0}, {50, "p1", 0}, {60100, "p1", 0}};
  for (size_t i = 0; i < sizeof steps / sizeof steps[0]; i++) {
    char label[32], value[16] = "";
    const char *how;
    if (steps[i].new_price > 0) {
      snprintf(label, sizeof label, "set %s to %d", steps[i].product,
               steps[i].new_price);
      how = set_price(steps[i].product, steps[i].new_price);
    } else {
      snprintf(label, sizeof label, "get %s", steps[i].product);
      how = get_price(steps[i].product, steps[i].ms, value);
    }
    printf("%6d ms  %-14s %4s  %s\n", steps[i].ms, label, value, how);
  }
  return 0;
}

In C: C's standard library has no hash map, so the database is one row and the cache one slot, enough for the one product this program caches; a delete empties the slot.

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

const TTL_MS = 60_000;
$database = ['p1' => 499]; // product id -> price
$cache = []; // product id -> [price, expires at]

/**
 * @param array<string, int> $database
 * @param array<string, array{int, int}> $cache
 * @return array{string, string}
 */
function getPrice(array $database, array &$cache, string $product, int $now): array
{
    $entry = $cache[$product] ?? null;
    if ($entry !== null && $entry[1] > $now) {
        return [(string) $entry[0], 'hit'];
    }
    $price = $database[$product] ?? null;
    if ($price === null) {
        return ['none', 'miss: no such product, nothing cached'];
    }
    $cache[$product] = [$price, $now + TTL_MS];
    return [(string) $price, $entry !== null
        ? 'miss: expired, read again and cached'
        : 'miss: read the database, cached for 60 s'];
}

/**
 * @param array<string, int> $database
 * @param array<string, array{int, int}> $cache
 * @return array{string, string}
 */
function setPrice(array &$database, array &$cache, string $product, int $price): array
{
    $database[$product] = $price; // 1. the database first: the source of truth
    unset($cache[$product]); // 2. then delete the copy: the next read reloads
    return ['', 'database updated, then the copy deleted'];
}

$steps = [[0, 'p1', null], [10, 'p1', null], [20, 'p9', null], [30, 'p1', 449],
    [40, 'p1', null], [50, 'p1', null], [60_100, 'p1', null]];
foreach ($steps as [$ms, $product, $newPrice]) {
    [$label, [$value, $how]] = $newPrice === null
        ? ["get {$product}", getPrice($database, $cache, $product, $ms)]
        : ["set {$product} to {$newPrice}", setPrice($database, $cache, $product, $newPrice)];
    printf("%6d ms  %-14s %4s  %s\n", $ms, $label, $value, $how);
}

In PHP: PHP copies an array passed by value, so the functions take the cache as &$cache to change the caller's; $cache[$product] ?? null reads a missing key without a warning.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

use std::collections::HashMap;

const TTL_MS: u32 = 60_000;

/// Prices read through a cache-aside cache, in front of a stand-in database.
struct Store {
    database: HashMap<&'static str, u32>,     // product id -> price
    cache: HashMap<&'static str, (u32, u32)>, // product id -> (price, expires at)
}

impl Store {
    fn get_price(&mut self, product: &'static str, now: u32) -> (String, &'static str) {
        let cached = self.cache.get(product).copied();
        if let Some((price, _)) = cached.filter(|&(_, expires_at)| expires_at > now) {
            return (price.to_string(), "hit");
        }
        let Some(&price) = self.database.get(product) else {
            return ("none".into(), "miss: no such product, nothing cached");
        };
        self.cache.insert(product, (price, now + TTL_MS));
        let how = match cached {
            Some(_) => "miss: expired, read again and cached",
            None => "miss: read the database, cached for 60 s",
        };
        (price.to_string(), how)
    }

    fn set_price(&mut self, product: &'static str, price: u32) -> (String, &'static str) {
        self.database.insert(product, price); // 1. the database first: the source of truth
        self.cache.remove(product); // 2. then delete the copy: the next read reloads
        (String::new(), "database updated, then the copy deleted")
    }
}

fn main() {
    let database = HashMap::from([("p1", 499)]);
    let mut store = Store {
        database,
        cache: HashMap::new(),
    };
    let steps = [
        (0, "p1", None),
        (10, "p1", None),
        (20, "p9", None),
        (30, "p1", Some(449)),
        (40, "p1", None),
        (50, "p1", None),
        (60_100, "p1", None),
    ];
    for (ms, product, new_price) in steps {
        let (label, (value, how)) = match new_price {
            Some(price) => (
                format!("set {product} to {price}"),
                store.set_price(product, price),
            ),
            None => (format!("get {product}"), store.get_price(product, ms)),
        };
        println!("{ms:>6} ms  {label:<14} {value:>4}  {how}");
    }
}

In Rust: copied() takes the cached pair out of the map, so the method can insert a new entry while it still knows whether the old one had expired; let-else returns early for a missing product.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

const TTL_MS = 60_000;
const database = new Map<string, number>([["p1", 499]]); // product id -> price
const cache = new Map<string, { price: number; expiresAt: number }>(); // product id -> entry

type Outcome = [value: string, how: string];

function getPrice(product: string, now: number): Outcome {
  const entry = cache.get(product);
  if (entry !== undefined && entry.expiresAt > now) return [String(entry.price), "hit"];
  const price = database.get(product);
  if (price === undefined) return ["none", "miss: no such product, nothing cached"];
  cache.set(product, { price, expiresAt: now + TTL_MS });
  if (entry !== undefined) {
    return [String(price), "miss: expired, read again and cached"];
  }
  return [String(price), "miss: read the database, cached for 60 s"];
}

function setPrice(product: string, price: number): Outcome {
  database.set(product, price); // 1. the database first: the source of truth
  cache.delete(product); // 2. then delete the copy: the next read reloads
  return ["", "database updated, then the copy deleted"];
}

const steps: [ms: number, product: string, newPrice?: number][] = [
  [0, "p1"],
  [10, "p1"],
  [20, "p9"],
  [30, "p1", 449],
  [40, "p1"],
  [50, "p1"],
  [60_100, "p1"],
];
for (const [ms, product, newPrice] of steps) {
  const label = newPrice === undefined ? `get ${product}` : `set ${product} to ${newPrice}`;
  const [value, how] = newPrice === undefined ? getPrice(product, ms) : setPrice(product, newPrice);
  console.log(`${String(ms).padStart(6)} ms  ${label.padEnd(14)} ${value.padStart(4)}  ${how}`);
}

In TypeScript: Each step is a tuple whose third element is optional, newPrice?: number, so a read simply leaves it out and the compiler checks every use.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import "fmt"

const ttlMs = 60_000

type entry struct{ price, expiresAt int }

var (
	database = map[string]int{"p1": 499} // product id -> price
	cache    = map[string]entry{}        // product id -> entry
)

func getPrice(product string, now int) (string, string) {
	e, cached := cache[product]
	if cached && e.expiresAt > now {
		return fmt.Sprint(e.price), "hit"
	}
	price, found := database[product]
	if !found {
		return "none", "miss: no such product, nothing cached"
	}
	cache[product] = entry{price, now + ttlMs}
	if cached {
		return fmt.Sprint(price), "miss: expired, read again and cached"
	}
	return fmt.Sprint(price), "miss: read the database, cached for 60 s"
}

func setPrice(product string, price int) (string, string) {
	database[product] = price // 1. the database first: the source of truth
	delete(cache, product)    // 2. then delete the copy: the next read reloads
	return "", "database updated, then the copy deleted"
}

func main() {
	// A new price of 0 means a read.
	steps := []struct {
		ms       int
		product  string
		newPrice int
	}{
		{0, "p1", 0}, {10, "p1", 0}, {20, "p9", 0}, {30, "p1", 449},
		{40, "p1", 0}, {50, "p1", 0}, {60_100, "p1", 0},
	}
	for _, s := range steps {
		label, value, how := "get "+s.product, "", ""
		if s.newPrice > 0 {
			label = fmt.Sprintf("set %s to %d", s.product, s.newPrice)
			value, how = setPrice(s.product, s.newPrice)
		} else {
			value, how = getPrice(s.product, s.ms)
		}
		fmt.Printf("%6d ms  %-14s %4s  %s\n", s.ms, label, value, how)
	}
}

In Go: Go has no optional values, so a new price of 0, the zero value, marks a read; a map lookup's second result says whether the key was there.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

let ttlMs = 60_000
var database = ["p1": 499]  // product id -> price
var cache: [String: (price: Int, expiresAt: Int)] = [:]  // product id -> entry

func getPrice(_ product: String, now: Int) -> (value: String, how: String) {
  let entry = cache[product]
  if let entry, entry.expiresAt > now {
    return (String(entry.price), "hit")
  }
  guard let price = database[product] else {
    return ("none", "miss: no such product, nothing cached")
  }
  cache[product] = (price, now + ttlMs)
  if entry != nil {
    return (String(price), "miss: expired, read again and cached")
  }
  return (String(price), "miss: read the database, cached for 60 s")
}

func setPrice(_ product: String, to price: Int) -> (value: String, how: String) {
  database[product] = price  // 1. the database first: the source of truth
  cache[product] = nil  // 2. then delete the copy: the next read reloads
  return ("", "database updated, then the copy deleted")
}

func padded(_ text: String, _ width: Int, left: Bool) -> String {
  let fill = String(repeating: " ", count: max(0, width - text.count))
  return left ? fill + text : text + fill
}

let steps: [(ms: Int, product: String, newPrice: Int?)] = [
  (0, "p1", nil), (10, "p1", nil), (20, "p9", nil), (30, "p1", 449),
  (40, "p1", nil), (50, "p1", nil), (60_100, "p1", nil),
]
for (ms, product, newPrice) in steps {
  let label = newPrice.map { "set \(product) to \($0)" } ?? "get \(product)"
  let out = newPrice.map { setPrice(product, to: $0) } ?? getPrice(product, now: ms)
  let time = padded(String(ms), 6, left: true)
  print(
    "\(time) ms  \(padded(label, 14, left: false)) \(padded(out.value, 4, left: true))  \(out.how)")
}

In Swift: newPrice.map { … } ?? … chooses the write or the read in one expression, and setting a dictionary key to nil deletes the cached copy.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# Prices read through a cache-aside cache, in front of a stand-in database.
class PriceStore
  TTL_MS = 60_000
  EXPIRED = 'miss: expired, read again and cached'
  LOADED = 'miss: read the database, cached for 60 s'

  def initialize
    @database = { 'p1' => 499 } # product id -> price
    @cache = {} # product id -> [price, expires at]
  end

  def get_price(product, now)
    price, expires_at = @cache[product]
    return [price.to_s, 'hit'] if expires_at && expires_at > now

    fresh = @database[product]
    return ['none', 'miss: no such product, nothing cached'] if fresh.nil?

    @cache[product] = [fresh, now + TTL_MS]
    [fresh.to_s, price ? EXPIRED : LOADED]
  end

  def set_price(product, price)
    @database[product] = price # 1. the database first: the source of truth
    @cache.delete(product) # 2. then delete the copy: the next read reloads
    ['', 'database updated, then the copy deleted']
  end
end

store = PriceStore.new
steps = [[0, 'p1'], [10, 'p1'], [20, 'p9'], [30, 'p1', 449], [40, 'p1'], [50, 'p1'], [60_100, 'p1']]
steps.each do |ms, product, new_price|
  label = new_price ? "set #{product} to #{new_price}" : "get #{product}"
  value, how = new_price ? store.set_price(product, new_price) : store.get_price(product, ms)
  puts "#{ms.to_s.rjust(6)} ms  #{label.ljust(14)} #{value.rjust(4)}  #{how}"
end

In Ruby: Hash#delete removes the cached copy and returns nil when there is none, and price, expires_at = @cache[product] unpacks a missing entry into two nils.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

const val TTL_MS = 60_000

val database = mutableMapOf("p1" to 499) // product id -> price
val cache = mutableMapOf<String, Pair<Int, Int>>() // product id -> (price, expires at)

data class Step(
    val ms: Int,
    val product: String,
    val newPrice: Int? = null,
)

fun getPrice(
    product: String,
    now: Int,
): Pair<String, String> {
    val entry = cache[product]
    if (entry != null && entry.second > now) return entry.first.toString() to "hit"
    val price = database[product] ?: return "none" to "miss: no such product, nothing cached"
    cache[product] = price to now + TTL_MS
    if (entry != null) return price.toString() to "miss: expired, read again and cached"
    return price.toString() to "miss: read the database, cached for 60 s"
}

fun setPrice(
    product: String,
    price: Int,
): Pair<String, String> {
    database[product] = price // 1. the database first: the source of truth
    cache.remove(product) // 2. then delete the copy: the next read reloads
    return "" to "database updated, then the copy deleted"
}

fun main() {
    val steps =
        listOf(
            Step(0, "p1"),
            Step(10, "p1"),
            Step(20, "p9"),
            Step(30, "p1", 449),
            Step(40, "p1"),
            Step(50, "p1"),
            Step(60_100, "p1"),
        )
    for ((ms, product, newPrice) in steps) {
        val label = if (newPrice == null) "get $product" else "set $product to $newPrice"
        val (value, how) =
            if (newPrice == null) getPrice(product, ms) else setPrice(product, newPrice)
        println("${ms.toString().padStart(6)} ms  ${label.padEnd(14)} ${value.padStart(4)}  $how")
    }
}

In Kotlin: The elvis operator, ?: return, leaves getPrice early when the database has no such product, and Step is a data class whose newPrice defaults to null, a read.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

const ttlMs = 60000;
final database = {'p1': 499}; // product id -> price
final cache = <String, ({int price, int expiresAt})>{}; // product id -> entry

(String, String) getPrice(String product, int now) {
  final entry = cache[product];
  if (entry != null && entry.expiresAt > now) return ('${entry.price}', 'hit');
  final price = database[product];
  if (price == null) return ('none', 'miss: no such product, nothing cached');
  cache[product] = (price: price, expiresAt: now + ttlMs);
  return entry != null
      ? ('$price', 'miss: expired, read again and cached')
      : ('$price', 'miss: read the database, cached for 60 s');
}

(String, String) setPrice(String product, int price) {
  database[product] = price; // 1. the database first: the source of truth
  cache.remove(product); // 2. then delete the copy: the next read reloads
  return ('', 'database updated, then the copy deleted');
}

void main() {
  const steps = <(int, String, int?)>[
    (0, 'p1', null),
    (10, 'p1', null),
    (20, 'p9', null),
    (30, 'p1', 449),
    (40, 'p1', null),
    (50, 'p1', null),
    (60100, 'p1', null),
  ];
  for (final (ms, product, newPrice) in steps) {
    final label = newPrice == null
        ? 'get $product'
        : 'set $product to $newPrice';
    final (value, how) = newPrice == null
        ? getPrice(product, ms)
        : setPrice(product, newPrice);
    print(
      '${'$ms'.padLeft(6)} ms  ${label.padRight(14)} ${value.padLeft(4)}  $how',
    );
  }
}

In Dart: Each operation returns a record of two strings, and final (value, how) = … takes the record apart in one declaration.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

     0 ms  get p1          499  miss: read the database, cached for 60 s
    10 ms  get p1          499  hit
    20 ms  get p9         none  miss: no such product, nothing cached
    30 ms  set p1 to 449        database updated, then the copy deleted
    40 ms  get p1          449  miss: read the database, cached for 60 s
    50 ms  get p1          449  hit
 60100 ms  get p1          449  miss: expired, read again and cached

Step through the same seven steps as messages between the app, the cache and the database:

Interactive · message by message The cache-aside program, one message at a time Step through the program's seven steps. The app talks to the cache and the database separately, and the sub-labels show what each one holds.
The cache-aside program, one message at a time, step 10 of 10. Messages so far: App to Cache: get p1; App to Database: read p1; App to Cache: set p1 = 499, 60 s; App to Cache: get p1; App to Database: read p9; App to Database: write p1 = 449; App to Cache: delete p1; App to Database: read p1; App to Cache: set p1 = 449, 60 s; App to Cache: get p1: expired.AppCachep1 = 449Databasep1 = 449get p1read p1set p1 = 499, 60 sget p1read p9write p1 = 449delete p1read p1set p1 = 449, 60 sget p1: expiredthe TTL is the safety net

The drawing shows the whole scene; its 10 steps are listed below.

Every step as text
  1. 0 ms: get p1. The app asks the cache for p1's price first. The cache is empty, so this read is a miss.
  2. Read the database. On a miss the app reads the database itself. The database returns 499.
  3. Store it with a TTL. The app stores 499 in the cache with a TTL of 60 seconds, so the copy expires at 60,000 ms, and returns the price.
  4. 10 ms: a hit. The next read finds p1 in the cache, a hit. The database does no work for it.
  5. 20 ms: p9 does not exist. p9 misses the cache, and the database has no such product, so the app caches nothing. Caching an absence would be a separate decision with its own lifetime.
  6. 30 ms: write the database. The write of a new price, 449, goes to the database first, because the database is the source of truth.
  7. Then delete the copy. Then the app deletes the cached copy instead of writing the new price into it. The cache holds nothing for p1 now.
  8. 40 ms: the next read misses. The read after the write misses, because the copy is gone, and the app reads 449 from the database.
  9. Store the new price. It stores 449 with a fresh 60-second TTL, until 60,040 ms. The read at 50 ms is a hit on this copy.
  10. 60,100 ms: the TTL has run out. Sixty seconds after the copy was stored it has expired. Without any write, the read misses, reloads 449 from the database and caches it again.

Two details are easy to get wrong. The write changes the database first and deletes the cached copy second: the database is the source of truth: The one store whose copy of a fact is authoritative. Every other store that holds the fact copies it from there and can be rebuilt from it. More in “Choosing a data model: relational to vector”, and the Azure pattern warns that the opposite order leaves a window in which a reader can put the old value back. And the read of p9, which does not exist, caches nothing; caching an absence is a separate decision with its own lifetime. The TTL is the safety net: even a copy that a missed delete leaves behind disappears within a minute.

Checkpoint

Check: Cache-aside's two rules in code

  1. Question 1 of 2 In the program, the read at 40 ms was a miss even though p1 had been read at 0 ms. What made it a miss?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The write at 30 ms updated the database and deleted the cached copy, so the next read loaded the new price

    Cache-aside deletes the cached copy after writing the database. The next read misses, reads 449 from the database and caches it. The TTL ran out only later: the read at 60,100 ms missed because the entry loaded at 40 ms had expired at 60,040 ms.

  2. Question 2 of 2 What could go wrong if the program's write deleted the cached copy first and wrote the database second?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A read between the two steps could load the old price from the database and put it back in the cache

    The database is the source of truth, so it changes first; deleting the copy second means any read that misses afterwards loads the new value. In the other order, a reader that misses between the delete and the write reloads the old price, and it stays until the TTL ends.

Practise: write get and put for a cache-aside store

Write the two rules yourself. The sample tests check a hit, the TTL, the delete after a write and a product the database does not have:

Exercise · Easy · JavaScript

Implement get and put for a cache-aside store

Write the two methods of createStore(db, ttlMs, clock) in store.mjs, a cache-aside store in front of a database.

  • db has get(key), which returns the stored value or undefined when there is none, and put(key, value).
  • ttlMs is how long an entry may be served from the cache, in milliseconds.
  • clock() returns the current time in milliseconds. Use it for every time you need, so the tests can move time on.
  • cache is the store's Map of entries, key → { value, expiresAt }. The tests look inside it.

get(key) returns the value. If the cache holds an entry for key and clock() is still before its expiresAt, it answers from the cache without touching the database. Otherwise it reads the database once, keeps the value with expiresAt set to clock() + ttlMs, and returns it. A key the database does not have returns undefined and is not cached.

put(key, value) writes the database first and then deletes the cached entry, so the next get loads the new value. It never writes the value into the cache itself.

Starter code · store.mjs

/** A cache-aside store in front of `db`: entries are served from the cache for `ttlMs` milliseconds of `clock()`. */
export function createStore(db, ttlMs, clock) {
  const cache = new Map(); // key -> { value, expiresAt }
  return {
    cache,
    get(key) {
      // Replace this line with your code.
      return undefined;
    },
    put(key, value) {
      // Replace this line with your code.
    },
  };
}
The sample tests · store.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { createStore } from './store.mjs';

/** A stand-in database that counts its reads and writes. */
function fakeDb(rows) {
  const data = new Map(Object.entries(rows));
  const db = {
    reads: 0,
    writes: 0,
    get(key) {
      db.reads++;
      return data.get(key);
    },
    put(key, value) {
      db.writes++;
      data.set(key, value);
    },
  };
  return db;
}

test('a miss reads the database once, then the cache answers', () => {
  const db = fakeDb({ p1: 499 });
  const store = createStore(db, 1000, () => 0);
  assert.equal(store.get('p1'), 499);
  assert.equal(store.get('p1'), 499);
  assert.equal(db.reads, 1);
});

test('an entry is served until its TTL ends, then read again', () => {
  let now = 0;
  const db = fakeDb({ p1: 499 });
  const store = createStore(db, 1000, () => now);
  store.get('p1');
  now = 999;
  store.get('p1');
  assert.equal(db.reads, 1);
  now = 1000;
  assert.equal(store.get('p1'), 499);
  assert.equal(db.reads, 2);
  assert.equal(store.cache.get('p1').expiresAt, 2000);
});

test('put writes the database, then deletes the cached copy', () => {
  const db = fakeDb({ p1: 499 });
  const store = createStore(db, 1000, () => 0);
  store.get('p1');
  store.put('p1', 449);
  assert.equal(db.writes, 1);
  assert.equal(store.cache.has('p1'), false);
  assert.equal(store.get('p1'), 449);
  assert.equal(db.reads, 2);
});

test('a key the database does not have is not cached', () => {
  const db = fakeDb({});
  const store = createStore(db, 1000, () => 0);
  assert.equal(store.get('p9'), undefined);
  assert.equal(store.get('p9'), undefined);
  assert.equal(db.reads, 2);
  assert.equal(store.cache.has('p9'), false);
});
A hint

Start with get. An entry may answer only while clock() is still before its expiresAt; anything else is a miss. On a miss, read the database once and decide what to do with undefined before you store anything. For put, the order of the two calls matters: the database is the source of truth, so it changes first.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then answer the questions below on the two paths, the four strategies and cache-aside. A question you miss is asked again before the session ends.

Practice that adapts to you

Practise this lesson

11 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 With cache-aside, who puts a value into the cache after a miss?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: The application, after it has read the value from the database

    In cache-aside the application owns both paths: it reads the cache, reads the database on a miss and then stores the value in the cache. When the cache loads the value itself, the strategy is read-through.

  2. Question 2 of 11 What does a read-through cache need that a cache-aside cache does not?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: Its own access to the database, to load a missing value by itself

    Read-through moves the loading into the cache layer, often a library or a caching product, so every caller gets the same behaviour without writing it, and the cache needs its own access to the database.

  3. Question 3 of 11 A cache write takes 1 ms and a database commit 6 ms. About how many milliseconds does a write-through write take?

    Type a number.

    A hint

    Under write-through, the write returns only after the database has it.

    Show the answer: Question 3 of 11

    Answer: 7 ms

    A write-through write takes as long as both writes together: 1 + 6 = 7 ms. That is why write-through does not improve write performance at all.

  4. Question 4 of 11 What does refresh-ahead cost?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Database reads for entries that nobody reads again

    Refresh-ahead reloads popular entries shortly before they expire, so readers rarely wait for a miss, and it pays with database reads for entries nobody reads again.

  5. Question 5 of 11 Writes arrive faster than the database can take them, and the business can lose a few seconds of them in a crash. Which write path does the comparison table point to?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: Write-back

    Write-back acknowledges as soon as the cache has a write and copies it later, combining several changes to one key into one database write. The price is the writes a cache failure can lose.

  6. Question 6 of 11 A team chooses a strategy that acknowledges writes before the database has them, to make writes fast. What do they pay for it?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Acknowledged writes are at risk until they reach the database

    The answers to the two questions have costs that show up elsewhere: filling the cache on writes makes writes slower, and delaying writes to the database makes them fast while putting acknowledged data at risk.

  7. Question 7 of 11 Service X caches product data with cache-aside and service Y with read-through, in the same cache cluster. The cluster fails for ten minutes. What do the users of each service see?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: X's users get slower pages served from the database; Y's users get errors

    A strategy that keeps the logic in the application gives the application a way around a failed cache; one that hides the database behind the cache does not.

  8. Question 8 of 11 In the program, why did the read of p9 at 20 ms cache nothing?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: p9 does not exist, and caching an absence is a separate decision with its own lifetime

    The read of p9 misses, reads the database, finds no such product and stores nothing. Caching an absence would stop repeated reads of a missing id from reaching the database, and needs its own short lifetime.

  9. Question 9 of 11 A delete after a write is lost in a network blip. With the program's TTL, for at most how many seconds can the stale copy be served?

    Type a number.

    Show the answer: Question 9 of 11

    Answer: 60 seconds

    The TTL is the safety net: even a copy that a missed delete leaves behind disappears within its 60-second lifetime, after which the next read loads the current price.

  10. Question 10 of 11 In the program's seven steps, how many times did the store read the database?

    Type a number.

    A hint

    Count the misses, including the read of a product that does not exist.

    Show the answer: Question 10 of 11

    Answer: 4 reads

    Every miss reads the database: p1 at 0 ms, p9 at 20 ms, p1 at 40 ms after the delete and p1 at 60,100 ms after the TTL ran out. The two hits at 10 and 50 ms read nothing, so four of the seven lines printed say miss.

  11. Question 11 of 11 A team drops the TTL from its cache-aside entries, because every write deletes the copy anyway. What can now happen?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: A copy that a missed delete leaves behind is served until something else removes it, possibly for days

    A TTL is the safety net under the delete: with one, even a copy that a missed delete leaves behind disappears within its lifetime; without one, it can stay until the entry is evicted or deleted.

Each strategy in depth, with its failure cases Pro: opens with a Pro, Premium or Ultimate pass

Every strategy fails in its own way, and most failures only show when two things happen at once or when the cache disappears. This part reproduces those failures in code that runs and counts what each one costs.

In this part:

  • Read and write paths of each strategy, step by step
  • Delete or update after a write: the race, reproduced
  • What a partition and a crash cost each strategy, measured
  • How long write-back leaves writes at risk
  • Cold starts and refresh-ahead under each strategy

2 diagrams · 2 runnable examples (Python) · about 17 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a unique ID generator

Choosing a strategy by workload, with numbers Pro: opens with a Pro, Premium or Ultimate pass

The right strategy follows from a few numbers you can find before writing any code: how many reads the database can take, how many writes arrive, and how stale or how lost a value may be. This part turns those numbers into a choice, with two worked scenarios and five more to decide.

In this part:

  • From the database’s limit to the hit ratio you need
  • Write-back’s flush interval against the writes at risk, worked
  • Matching each requirement to a strategy
  • Worked scenario: a product catalogue
  • Worked scenario: a view counter
  • Five scenarios to decide

1 diagram · about 14 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    Cache-aside or read-through: who loads data into the cache on a miss?

    In cache-aside the application does: it reads the cache, and on a miss it reads the database itself and writes the value into the cache. In read-through the cache does: the application asks only the cache, and the cache calls a loader that reads the database. The cached data can be exactly the same; what differs is where the code lives and what happens when the cache fails. A cache-aside application can fall back to the database when the cache is down, while a read-through caller has no other path to the data. Read-through keeps one loading rule for every caller; cache-aside lets each caller decide what to cache and for how long.

  2. Mid to senior

    Product pages are read 50,000 times a second and the database serves at most 8,000 reads a second. What do you cache, and how do you keep it correct?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A video view counter uses write-back: increments go to the cache and are flushed to the database every 10 seconds. How many views can you lose, and when is that acceptable?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    During a sale, stock counts are read 30,000 times a second and change 2,000 times a second. Would you use cache-aside, write-through or write-back for them, and what goes wrong with each?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A strategy is a rule for the read path and a rule for the write path: who fills a miss, and what a write does to the cached copy.
  • Cache-aside: the application reads the cache, loads misses from the database, writes the database and then deletes the cached copy.
  • Read-through and write-through put the cache in the data path: consistent behaviour, and no way around a failed cache.
  • Write-back acknowledges writes before the database has them: fast, and a cache failure can lose them.
  • Write the database first, then delete; give every entry a TTL as a safety net.

Terms in this lesson

cache-aside
A caching strategy in which the application does the work: it reads the cache, loads a miss from the database and stores it, and after a write it updates the database and deletes the cached copy.
read-through
A caching strategy in which the cache loads a missing value from the database by itself, so every caller shares one loading rule and has no other path to the data while the cache is down.
refresh-ahead
A cache that reloads popular entries shortly before they expire, so readers rarely wait for a miss, at the cost of database reads for entries nobody reads again.
TTL (cache entry)
Time to live: how long a cache may serve an entry after storing it. When it runs out the next read is a miss, so even a copy that a missed delete left behind disappears.
write-back (write-behind)
A caching strategy in which the cache acknowledges a write at once and copies it to the database later, so a cache failure can lose writes that were already acknowledged.
write-through
A caching strategy in which a write goes to the cache and the database before it is acknowledged, so a read right after it finds the new value, and each write takes as long as both writes together.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.