Machine Coding Problem

In-Memory Cache (LRU/LFU)

maco60macoAllinfrastructurestrategy-patterngenericsconcurrent-read-write-lockseviction-mechanics
Commonly Asked By:RedisLabsAmazonTwitterGoogle

Functional Scope (In-Scope)

  • Strategy-driven Eviction: Support multiple eviction mechanics (LRU and LFU) using the Strategy Pattern.
  • Dynamic TTL Expired Invalidation: Cleanly check and invalidate cache entries lazily upon query.
  • Multi-threaded Locks: Shield backing dictionaries with concurrent read-write synchronization locks.
  • Telemetry Tracking Stats: Aggregate hit, miss, and eviction totals per-cache instance.

Explicit Boundaries (Out-of-Scope)

  • No Cluster Node Replica Sync: Bypasses state serialization across multiple servers.
  • No Swap Disk Spillage: Restricts operations memory capacity strictly to RAM storage heaps.

Clean reference designs demonstrating interchangeable eviction caches in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.*;

interface EvictionStrategy<K> {
    void onAccess(K key);
    void onPut(K key);
    void onRemove(K key);
    K evict();
}

class LRUEvictionStrategy<K> implements EvictionStrategy<K> {
    private final LinkedHashSet<K> order = new LinkedHashSet<>();

    @Override
    public void onAccess(K key) {
        order.remove(key);
        order.add(key);
    }

    @Override
    public void onPut(K key) {
        order.add(key);
    }

    @Override
    public void onRemove(K key) {
        order.remove(key);
    }

    @Override
    public K evict() {
        if (order.isEmpty()) return null;
        K oldest = order.iterator().next();
        order.remove(oldest);
        return oldest;
    }
}

class LFUEvictionStrategy<K> implements EvictionStrategy<K> {
    private final Map<K, Integer> counts = new HashMap<>();
    private final Map<K, Long> timestamps = new HashMap<>();

    @Override
    public void onAccess(K key) {
        counts.put(key, counts.getOrDefault(key, 0) + 1);
        timestamps.put(key, System.nanoTime());
    }

    @Override
    public void onPut(K key) {
        counts.putIfAbsent(key, 1);
        timestamps.put(key, System.nanoTime());
    }

    @Override
    public void onRemove(K key) {
        counts.remove(key);
        timestamps.remove(key);
    }

    @Override
    public K evict() {
        if (counts.isEmpty()) return null;
        K lfuKey = null;
        int minCount = Integer.MAX_VALUE;
        long minTime = Long.MAX_VALUE;

        for (Map.Entry<K, Integer> entry : counts.entrySet()) {
            K key = entry.getKey();
            int count = entry.getValue();
            long time = timestamps.get(key);
            if (count < minCount || (count == minCount && time < minTime)) {
                minCount = count;
                minTime = time;
                lfuKey = key;
            }
        }
        if (lfuKey != null) {
            counts.remove(lfuKey);
            timestamps.remove(lfuKey);
        }
        return lfuKey;
    }
}

class CacheEntry<V> {
    private final V value;
    private final long expiryTime;

    public CacheEntry(V value, long ttlMs) {
        this.value = value;
        this.expiryTime = ttlMs > 0 ? System.currentTimeMillis() + ttlMs : Long.MAX_VALUE;
    }

    public V getValue() { return value; }
    public boolean isExpired() { return System.currentTimeMillis() > expiryTime; }
}

class CacheStats {
    private final LongAdder hits = new LongAdder();
    private final LongAdder misses = new LongAdder();
    private final LongAdder evictions = new LongAdder();

    public void recordHit() { hits.increment(); }
    public void recordMiss() { misses.increment(); }
    public void recordEviction() { evictions.increment(); }

    public long getHits() { return hits.sum(); }
    public long getMisses() { return misses.sum(); }
    public long getEvictions() { return evictions.sum(); }
}

class InMemoryCache<K, V> {
    private final int capacity;
    private final Map<K, CacheEntry<V>> store = new HashMap<>();
    private final EvictionStrategy<K> evictionStrategy;
    private final CacheStats stats = new CacheStats();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public InMemoryCache(int capacity, EvictionStrategy<K> strategy) {
        this.capacity = capacity;
        this.evictionStrategy = strategy;
    }

    public V get(K key) {
        rwLock.writeLock().lock();
        try {
            CacheEntry<V> entry = store.get(key);
            if (entry == null) {
                stats.recordMiss();
                return null;
            }
            if (entry.isExpired()) {
                store.remove(key);
                evictionStrategy.onRemove(key);
                stats.recordMiss();
                return null;
            }
            evictionStrategy.onAccess(key);
            stats.recordHit();
            return entry.getValue();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void put(K key, V value, long ttlMs) {
        rwLock.writeLock().lock();
        try {
            if (store.containsKey(key)) {
                store.put(key, new CacheEntry<>(value, ttlMs));
                evictionStrategy.onAccess(key);
            } else {
                if (store.size() >= capacity) {
                    K evictedKey = evictionStrategy.evict();
                    if (evictedKey != null) {
                        store.remove(evictedKey);
                        stats.recordEviction();
                    }
                }
                store.put(key, new CacheEntry<>(value, ttlMs));
                evictionStrategy.onPut(key);
            }
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public CacheStats getStats() { return stats; }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== In-Memory Cache (LRU Strategy) ===");
        InMemoryCache<String, String> lruCache = new InMemoryCache<>(2, new LRUEvictionStrategy<>());
        lruCache.put("A", "Apple", 0);
        lruCache.put("B", "Banana", 0);
        lruCache.get("A");
        lruCache.put("C", "Cherry", 0); // Evicts B

        System.out.println("Hits: " + lruCache.getStats().getHits());
        System.out.println("Misses: " + lruCache.getStats().getMisses());
        System.out.println("Evictions: " + lruCache.getStats().getEvictions());
    }
}