InterviewKata
System design · Technology guide T02 · Builds on Module 09

In-memory caches and data stores

"Put Redis in front of it" is the most common answer in system design interviews, and often the right one. But Redis now has a fork, Valkey, under a different licence; Memcached is still the simplest choice for plain caching; every cloud sells its own managed version; and Azure is retiring one of its Redis services. This guide explains how each option works, how they differ, and which to choose, with a chooser and code you can run.

A 3-minute conversation: Sam asks, Priya explains
0:00 / 2:37

1. What it is

An in-memory store keeps data in RAM, so a read takes well under a millisecond instead of the several milliseconds a database query costs. Used as a cache, it holds copies of data that live elsewhere; used as a data store, it holds data of its own (sessions, rate-limit counters, leaderboards, queues). Module 09 teaches the caching patterns, invalidation and eviction; this guide is about the products.

LayerWhere it livesStrengthWeakness
In-process cacheInside each app server's memoryFastest: no network hopEvery server has its own copy, so updates leave stale copies elsewhere
Shared cacheA separate cache service all servers useOne copy, so one invalidation reaches everyoneA network hop, and one more system to run

2. How it works

One request for a product price, through two cache layers and a database.

3. The products

Data store · source-available and AGPL

Redis

A single thread executes commands against rich data structures (strings, hashes, lists, sets, sorted sets, streams), with I/O threads helping with network work. Persistence by snapshots (RDB) and an append-only file; replication, and Redis Cluster for sharding. Since Redis 8, Redis Open Source is licensed under your choice of RSALv2, SSPLv1 or AGPLv3.

Best for: caching plus data structures (leaderboards, rate limits, queues), when its licence suits you.

Data store · open source (BSD)

Valkey

A fork of Redis created in 2024 after Redis changed its licence, now backed by the Linux Foundation under the BSD licence. It keeps Redis's commands and protocol, so Redis clients work unchanged. The latest release is 9.1.3 (October 2026). The managed services on AWS and Google Cloud offer it alongside or instead of Redis.

Best for: Redis-style workloads where a permissive open-source licence matters.

Cache · open source

Memcached

A multi-threaded key-value cache for strings only, with a slab allocator and no persistence or replication. Clients spread keys across servers with consistent hashing.

Best for: plain, large, simple caches where data structures and durability don't matter.

Data store · BSL 1.1

Dragonfly

A multi-threaded in-memory store that speaks the Redis and Memcached protocols, aiming for more throughput per machine. Released under the Business Source License 1.1, which isn't an open-source licence.

Best for: very high throughput on a single large machine, if the licence fits.

Library · in process

Caffeine, Ristretto and similar

Caches inside your application (Caffeine for the JVM, Ristretto for Go), using admission policies such as TinyLFU to keep the most useful entries.

Best for: hot, rarely changing data such as configuration, or a small first layer in front of a shared cache.

Managed · Google Cloud

Memorystore

Google Cloud's managed in-memory service, offered as Memorystore for Valkey, Memorystore for Redis Cluster and Memorystore for Redis. Memorystore for Memcached is marked deprecated.

Best for: Redis or Valkey on Google Cloud without running servers; choose Valkey or Redis Cluster to scale out.

Managed · AWS

Amazon ElastiCache

Managed Valkey, Redis OSS and Memcached, either serverless (capacity scales automatically) or as node-based clusters you size yourself. Node-based Valkey clusters can enable durability, persisting data in a multi-AZ transaction log so it survives every node failing.

Best for: caching on AWS; serverless when traffic is unpredictable.

Managed · Azure

Azure Managed Redis (and Azure Cache for Redis)

Microsoft is retiring Azure Cache for Redis: the Enterprise tiers on 31 March 2027 and the Basic, Standard and Premium tiers on 30 September 2028. Azure Managed Redis is the replacement, with migration tooling in preview since April 2026.

Best for: new Redis workloads on Azure, on Azure Managed Redis.

4. Differences at a glance

OptionData modelThreadingPersistenceScaling outLicence
RedisRich data structuresOne command thread, I/O threadsRDB snapshots and AOFRedis Cluster (hash slots)RSALv2, SSPLv1 or AGPLv3 (Redis 8+)
ValkeySame as RedisSame modelRDB and AOFCluster modeBSD
MemcachedStrings onlyMulti-threadedNoneClient-side shardingBSD
DragonflyRedis and Memcached APIsMulti-threadedSnapshotsMainly verticalBSL 1.1
In-processObjects in your languageYour app'sNoneOne copy per serverLibrary's own
MemorystoreValkey or RedisManagedEngine and tier dependentValkey and Redis ClusterManaged service
ElastiCacheValkey, Redis OSS or MemcachedManagedOptional durability (node-based Valkey)Serverless, or cluster modeManaged service
Azure Managed RedisRedisManagedTier dependentClusteredManaged service

5. When to choose which, and why

  1. Do the servers need to agree? If a stale copy for a few seconds is fine (configuration, reference data), an in-process cache is fastest. If an update must be seen everywhere at once, use a shared cache.
  2. Only caching, or data structures too? Plain key-value caching works well on Memcached. Sorted sets, counters with expiry, streams and pub/sub point to Redis or Valkey.
  3. Licence: if you run it yourself and need an OSI open-source licence without copyleft obligations, Valkey (BSD). Redis 8+ offers AGPLv3 among its options; check what it means for your use.
  4. On a cloud, use its managed service: Memorystore, ElastiCache or Azure Managed Redis. Prefer Valkey where offered unless you need a Redis-only feature.
  5. Must the data survive a restart? A cache by definition can be rebuilt. If losing it loses information, either enable a durable option (such as ElastiCache's durability for Valkey) or keep it in a database.

Try it: choose for your requirements

6. Local vs shared caches, in code

A small LRU cache with a time to live, used two ways. With a local cache in each app server, server B keeps serving the old price until its TTL expires, because server A's invalidation only cleared A's own copy. With one shared cache, a single invalidation reaches everyone. The last line shows least-recently-used eviction at work. The program was run in all three languages; each prints the output below it.

from collections import OrderedDict


class LruCache:
    """An LRU cache with a time to live, like a Redis/Valkey key with EXPIRE, or a local in-process cache."""

    def __init__(self, capacity, ttl):
        self.items, self.capacity, self.ttl = OrderedDict(), capacity, ttl
        self.hits = self.misses = self.evictions = 0

    def get(self, key, now):
        item = self.items.get(key)
        if item and item[1] > now:
            self.items.move_to_end(key)          # most recently used
            self.hits += 1
            return item[0]
        self.misses += 1
        return None

    def set(self, key, value, now):
        self.items[key] = (value, now + self.ttl)
        self.items.move_to_end(key)
        if len(self.items) > self.capacity:
            self.items.popitem(last=False)       # evict the least recently used
            self.evictions += 1

    def delete(self, key):
        self.items.pop(key, None)


DB = {"price:42": 100}


def read(cache, key, now):
    """Cache-aside: try the cache, fall back to the database, then fill the cache."""
    value = cache.get(key, now)
    if value is None:
        value = DB[key]
        cache.set(key, value, now)
    return value


# Two app servers with their own local caches, against one shared cache (Redis, Valkey, Memorystore...).
local_a, local_b, shared = LruCache(3, ttl=60), LruCache(3, ttl=60), LruCache(3, ttl=60)
for c in (local_a, local_b, shared):
    read(c, "price:42", now=0)

DB["price:42"] = 90                              # server A updates the price at t=10 and invalidates its cache
local_a.delete("price:42")
shared.delete("price:42")
print("t=20 local caches: A sees", read(local_a, "price:42", 20), "| B sees", read(local_b, "price:42", 20), "(stale until its TTL)")
print("t=20 shared cache: A sees", read(shared, "price:42", 20), "| B sees", read(shared, "price:42", 20))
print("t=61 local cache B sees", read(local_b, "price:42", 61), "(TTL expired, refetched)")

lru = LruCache(3, ttl=1000)
for k in ["a", "b", "c", "a", "d", "b", "e", "a"]:
    if lru.get(k, 0) is None:
        lru.set(k, k.upper(), 0)
print(f"LRU, capacity 3: {lru.hits} hits, {lru.misses} misses, {lru.evictions} evictions; kept {', '.join(lru.items)}")
class LruCache {
  // An LRU cache with a time to live, like a Redis/Valkey key with EXPIRE, or a local in-process cache.
  items = new Map<string, [number | string, number]>();   // Map keeps insertion order: oldest first
  hits = 0; misses = 0; evictions = 0;
  constructor(private capacity: number, private ttl: number) {}

  get(key: string, now: number) {
    const item = this.items.get(key);
    if (item && item[1] > now) {
      this.items.delete(key); this.items.set(key, item);   // most recently used
      this.hits++;
      return item[0];
    }
    this.misses++;
    return undefined;
  }

  set(key: string, value: number | string, now: number) {
    this.items.delete(key);
    this.items.set(key, [value, now + this.ttl]);
    if (this.items.size > this.capacity) {
      this.items.delete(this.items.keys().next().value!);  // evict the least recently used
      this.evictions++;
    }
  }

  delete(key: string) { this.items.delete(key); }
}

const DB: Record<string, number> = { "price:42": 100 };

function read(cache: LruCache, key: string, now: number) {
  // Cache-aside: try the cache, fall back to the database, then fill the cache.
  let value = cache.get(key, now);
  if (value === undefined) { value = DB[key]; cache.set(key, value, now); }
  return value;
}

// Two app servers with their own local caches, against one shared cache (Redis, Valkey, Memorystore...).
const localA = new LruCache(3, 60), localB = new LruCache(3, 60), shared = new LruCache(3, 60);
for (const c of [localA, localB, shared]) read(c, "price:42", 0);

DB["price:42"] = 90;                             // server A updates the price at t=10 and invalidates its cache
localA.delete("price:42");
shared.delete("price:42");
console.log(`t=20 local caches: A sees ${read(localA, "price:42", 20)} | B sees ${read(localB, "price:42", 20)} (stale until its TTL)`);
console.log(`t=20 shared cache: A sees ${read(shared, "price:42", 20)} | B sees ${read(shared, "price:42", 20)}`);
console.log(`t=61 local cache B sees ${read(localB, "price:42", 61)} (TTL expired, refetched)`);

const lru = new LruCache(3, 1000);
for (const k of ["a", "b", "c", "a", "d", "b", "e", "a"]) if (lru.get(k, 0) === undefined) lru.set(k, k.toUpperCase(), 0);
console.log(`LRU, capacity 3: ${lru.hits} hits, ${lru.misses} misses, ${lru.evictions} evictions; kept ${[...lru.items.keys()].join(", ")}`);
import java.util.*;

public class Main {
    static class LruCache {
        // An LRU cache with a time to live, like a Redis/Valkey key with EXPIRE, or a local in-process cache.
        record Item(Object value, int expires) {}
        final LinkedHashMap<String, Item> items = new LinkedHashMap<>(16, 0.75f, true);   // access order: oldest first
        final int capacity, ttl;
        int hits, misses, evictions;
        LruCache(int capacity, int ttl) { this.capacity = capacity; this.ttl = ttl; }

        Object get(String key, int now) {
            Item item = items.get(key);                  // a get also marks it most recently used
            if (item != null && item.expires() > now) { hits++; return item.value(); }
            misses++;
            return null;
        }

        void set(String key, Object value, int now) {
            items.put(key, new Item(value, now + ttl));
            if (items.size() > capacity) {
                items.remove(items.keySet().iterator().next());   // evict the least recently used
                evictions++;
            }
        }

        void delete(String key) { items.remove(key); }
    }

    static final Map<String, Integer> DB = new HashMap<>(Map.of("price:42", 100));

    static Object read(LruCache cache, String key, int now) {
        // Cache-aside: try the cache, fall back to the database, then fill the cache.
        Object value = cache.get(key, now);
        if (value == null) { value = DB.get(key); cache.set(key, value, now); }
        return value;
    }

    public static void main(String[] args) {
        // Two app servers with their own local caches, against one shared cache (Redis, Valkey, Memorystore...).
        LruCache localA = new LruCache(3, 60), localB = new LruCache(3, 60), shared = new LruCache(3, 60);
        for (LruCache c : List.of(localA, localB, shared)) read(c, "price:42", 0);

        DB.put("price:42", 90);                          // server A updates the price at t=10 and invalidates its cache
        localA.delete("price:42");
        shared.delete("price:42");
        System.out.println("t=20 local caches: A sees " + read(localA, "price:42", 20) + " | B sees " + read(localB, "price:42", 20) + " (stale until its TTL)");
        System.out.println("t=20 shared cache: A sees " + read(shared, "price:42", 20) + " | B sees " + read(shared, "price:42", 20));
        System.out.println("t=61 local cache B sees " + read(localB, "price:42", 61) + " (TTL expired, refetched)");

        LruCache lru = new LruCache(3, 1000);
        for (String k : List.of("a", "b", "c", "a", "d", "b", "e", "a")) if (lru.get(k, 0) == null) lru.set(k, k.toUpperCase(), 0);
        System.out.println("LRU, capacity 3: " + lru.hits + " hits, " + lru.misses + " misses, " + lru.evictions + " evictions; kept " + String.join(", ", lru.items.keySet()));
    }
}
t=20 local caches: A sees 90 | B sees 100 (stale until its TTL)
t=20 shared cache: A sees 90 | B sees 90
t=61 local cache B sees 90 (TTL expired, refetched)
LRU, capacity 3: 1 hits, 7 misses, 4 evictions; kept b, e, a

7. Say it in the interview

Two-sentence answers
  • "Redis or Memcached?" Memcached for simple, large key-value caching with multi-threaded throughput; Redis or Valkey when I need data structures, expiry on counters, persistence or replication. On a cloud I'd use the managed service, preferring Valkey where it's offered.
  • "Local or distributed cache?" Local for hot, rarely changing data where a short stale window is fine; a shared cache when an update must be seen everywhere. Often both: a small local layer in front of a shared one.
  • "Can you use the cache as the database?" Only if the data can be rebuilt, or with a durable option enabled. Otherwise the database stays the source of truth.

8. Common mistakes

Local caches with no plan for stalenessEach server invalidates only its own copy. Use short TTLs, a shared cache, or broadcast invalidations.
Treating a cache as the source of truthA restart, eviction or failover loses data. Keep the truth in a database unless durability is enabled and tested.
Ignoring licences when self-hostingRedis's licence changed in 2024 and again with Redis 8. Check it, or choose Valkey.
Building on a retiring serviceNew Redis work on Azure belongs on Azure Managed Redis, not Azure Cache for Redis.
One slow command blocks everyoneRedis and Valkey run commands on one thread; avoid KEYS and huge range reads in production (Module 09).

9. Check yourself

Two app servers each keep a local cache. Server A updates a price and clears its cache. What does server B serve?

Local caches are per server; one invalidation doesn't reach the others.

You need a leaderboard with sorted scores and expiring counters. Which fits?

Sorted sets and per-key expiry are Redis-style data structures.

Why might a team choose Valkey over Redis for self-hosting?

The licence is the main difference; clients work unchanged.

Starting a new Redis workload on Azure today, which service?

Azure Cache for Redis tiers are being retired in 2027 and 2028.

10. Sources and further reading

Product facts checked on 4 November 2026. Licences, versions and retirement dates change; check before relying on them.

SourceUsed for
Redis licencesRSALv2, SSPLv1 or AGPLv3 from Redis 8
ValkeyBSD licence, Linux Foundation, latest release
Memorystore documentationMemorystore for Valkey, Redis Cluster and Redis; Memcached deprecated
What is Amazon ElastiCache?Engines, serverless and node-based clusters, durability
Azure Cache for Redis retirement FAQRetirement dates and Azure Managed Redis
Dragonfly licenceBSL 1.1
Module 09: CachingPatterns, invalidation, eviction, stampedes, hot keys