Interview preparation guide

Redis Interview Questions: Cache-Aside, Invalidation and TTL Exercises

This original Redis exercise uses a fictional product record to make cache behavior visible. You will observe a miss, a hit, a stale value, invalidation and expiration metadata. It also asks what the example cannot guarantee when reads and writes happen concurrently. The cache is an optimization; the in-memory source dictionary stands in for an authoritative store.

What happens in cache-aside reads?

The example first asks Redis for a versioned product key. On a miss, it reads the source and attempts to cache the value with an expiration. It returns the source result even if the cache operation fails. This is a specific example policy; it is not suitable for treating a cache as an authoritative source of permissions or financial records.

A JSON string is enough for this exercise because the entire small product record is read together. If the workload needed independent field updates, ordered rankings or membership checks, discuss the operations before choosing a Redis data structure.

Run an isolated local exercise

Install a local Redis server and the Python redis client in an isolated environment. Save the following as redis_exercise.py and run python redis_exercise.py on a system that supports Unix sockets. The script starts its own temporary Redis process with TCP disabled and persistence off, then terminates it and removes the temporary directory. It does not connect to an existing application cache.

The exact exercise passed with Python 3.12.14, redis-py 8.1.0 and Redis Server 8.6.2. It prints “Redis exercise passed: hit, stale value, invalidation, TTL, fallback”. The short startup loop has bounded attempts and disabled client retries so an unavailable local process does not trigger an extended retry cycle.

import json
import shutil
import subprocess
import tempfile
import time
from pathlib import Path
import redis
from redis.retry import Retry
from redis.backoff import NoBackoff


def get_product(cache, source, product_id):
    key = f'interview:product:v1:{product_id}'
    try:
        cached = cache.get(key)
        if cached is not None:
            return json.loads(cached), 'cache'
    except (redis.RedisError, json.JSONDecodeError):
        pass
    product = dict(source[product_id])
    try:
        cache.set(key, json.dumps(product), ex=60)
    except redis.RedisError:
        pass
    return product, 'source'


class UnavailableCache:
    def get(self, key):
        raise redis.ConnectionError('simulated unavailable cache')

    def set(self, *args, **kwargs):
        raise redis.ConnectionError('simulated unavailable cache')


server = shutil.which('redis-server')
if not server:
    raise SystemExit('Install a local Redis server before running this exercise')
with tempfile.TemporaryDirectory(prefix='redis-interview-', dir='/tmp') as folder:
    socket = str(Path(folder) / 'redis.sock')
    process = subprocess.Popen([
        server, '--port', '0', '--unixsocket', socket,
        '--unixsocketperm', '700', '--save', '', '--appendonly', 'no',
        '--dir', folder,
    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    client = redis.Redis(
        unix_socket_path=socket, decode_responses=True,
        socket_connect_timeout=0.2, socket_timeout=0.2,
        retry=Retry(NoBackoff(), 0),
    )
    try:
        for _ in range(100):
            if process.poll() is not None:
                raise RuntimeError("temporary Redis exited before becoming ready")
            try:
                if client.ping():
                    break
            except redis.ConnectionError:
                time.sleep(0.02)
        else:
            raise RuntimeError('temporary Redis did not start')
        source = {7: {'id': 7, 'name': 'Notebook'}}
        key = 'interview:product:v1:7'
        first, origin = get_product(client, source, 7)
        assert first['name'] == 'Notebook' and origin == 'source'
        assert get_product(client, source, 7)[1] == 'cache'
        assert 0 <= client.ttl(key) <= 60
        source[7]['name'] = 'Updated notebook'
        assert get_product(client, source, 7)[0]['name'] == 'Notebook'
        client.delete(key)
        assert get_product(client, source, 7)[0]['name'] == 'Updated notebook'
        client.set(key, json.dumps(source[7]))
        assert client.ttl(key) == -1  # SET without EX removed the previous TTL
        client.set(key, json.dumps(source[7]), ex=60)
        assert 0 <= client.ttl(key) <= 60
        client.pexpire(key, 0)  # deterministic immediate expiry, no timing benchmark
        assert client.ttl(key) == -2
        assert get_product(client, source, 7)[1] == 'source'
        assert get_product(UnavailableCache(), source, 7)[1] == 'source'
        print('Redis exercise passed: hit, stale value, invalidation, TTL, fallback')
    finally:
        client.close()
        process.terminate()
        process.wait(timeout=5)

Why does the updated source still return the old name?

Changing the source dictionary does not automatically change the cached value. The assertion deliberately observes “Notebook” after the source becomes “Updated notebook.” Deleting the exact key forces the next read back to the source.

This demonstrates a stale-value problem and one invalidation step. It does not prove that deletion is enough for concurrent production traffic. For example, a reader could fetch an old source value before a writer commits, then repopulate the cache after the writer deletes the key.

Reason about the race

Reader R: cache miss, reads the old source record.

Writer W: commits the new source record and deletes the cache key.

Reader R: writes its old result into the now-empty cache.

Interview response: “I would define an acceptable staleness window and consider version checks or a coordinated invalidation design. I would test the race rather than claiming a TTL makes the cache immediately consistent.”

What do TTL values -1 and -2 mean?

Redis documents -1 as a key with no expiration and -2 as a key that does not exist. In the exercise, SET without an expiry replaces the cached value and removes its previous TTL. The next SET includes ex=60, restoring the expiration. SET also supports options for preserving or setting expiration; choose deliberately rather than assuming updates retain it.

PEXPIRE with zero removes the key immediately, allowing a deterministic check without waiting for a wall-clock timeout. This confirms the immediate-expiration path, not a performance benchmark or a measurement of background expiration timing.

Explain the repair

Bug: a cache write uses SET key value after an earlier write used a TTL. The value now persists without that expiration.

Repair in this example: include ex=60 on every cache-fill write.

Boundary: TTL alone does not enforce a maximum key count or memory budget. Discuss memory limits and eviction policy separately for a real service.

What should happen when Redis is unavailable?

UnavailableCache is a small test double that raises Redis connection errors for reads and writes. The last assertion verifies that this function returns the source value under that simulated condition. It does not prove that an actual network outage is fast or that the source can absorb the extra traffic.

A production design needs timeouts, an intentional retry policy, observability and a plan for increased source load. Avoid returning invented default data merely to hide an outage. This function lets a missing source ID raise an error because “not found” handling is outside the tiny contract.

Further questions to answer aloud

Would you cache missing products? If so, specify the representation and shorter freshness policy, and explain how a later creation becomes visible.

How would you prevent many simultaneous misses from overwhelming the source? Discuss request coalescing or another workload-appropriate mechanism, then identify failure and timeout behavior.

How would you test malformed cache data? This function falls back on a JSON decoding error, but it does not validate the schema of successfully decoded JSON. Add schema validation if untrusted or incompatible values can appear.

Explain what the exercise establishes

The assertions show the chosen cache flow and Redis expiration commands on one temporary local instance. They do not verify clustering, replication, failover, eviction under pressure or distributed consistency. In an interview, naming those limits is part of a precise answer.

A strong follow-up discussion starts with the product’s freshness and availability requirements, then chooses cache behavior and tests that address those requirements. It does not start by assuming every read must hit Redis.

Questions, answered

Does expiration keep the cache immediately consistent?

No. A cached value can be stale until it is refreshed or invalidated, and concurrent operations can create additional races.

Does SET keep a previous TTL by default?

A normal SET replaces the value and clears the previous expiration. Use an explicit expiration or an appropriate preservation option when that is the intended behavior.

Does the outage test disconnect a real Redis server?

No. The outage assertion uses a test double. The exercise separately uses a real temporary Redis instance for hits, invalidation and expiration behavior.

Sources and examples

ResumizeAI team. Examples are fictional teaching material, not customer results. Replace sample details with your own facts.

Your experience. A complete resume.

Build in Resumize with ATS-friendly templates, guided editing and a live preview. Start free, then upgrade when you need more.

Free account required. Free-plan PDF exports include a watermark.