Back to LLD explorer
Structural• Patterns: Proxy, Strategy, Singleton

Cache (LRU / LFU)

Medium

Problem Summary

Design a high-performance in-memory Cache engine supporting O(1) reads and writes, configurable eviction strategies (LRU, LFU), thread safety, and transparent database proxy shielding.

Functional Scope

  • O(1) time complexity for get(key) and put(key, value).
  • Automatic eviction of the least recently used (LRU) or least frequently used (LFU) item upon reaching capacity.
  • Thread safety with high read throughput using ReadWriteLock or segmented striping.
  • Pluggable EvictionStrategy interface.
  • Optional TTL (time-to-live) expiration.

Entity-Relationship (ER) Schema

CacheProxy [1] --> [1] CacheEngine
CacheEngine [1] --> [1] EvictionStrategy
LRUCache [1] --> [1] DoublyLinkedList
LRUCache [1] --> [1] HashMap

Design Approach

Use a Doubly Linked List coupled with a Hash Table to achieve O(1) get and put operations. Protect concurrency using read/write locks so cache hits can execute concurrently.

Core Classes & Models

Cache (General interface: get, put, remove)LRUCache (Doubly linked list + Hash map implementation)Node (Key, value, prev, next references)EvictionStrategy (Pluggable eviction interface)CacheProxy (Transparent cache-aside database wrapper)
Code Blueprint
102 lines
/**
 * Low-Level Design: In-Memory Cache System
 * Design Patterns Applied:
 *  - Proxy Pattern: Cache proxy intercepting database queries with transparent read-through
 *  - Strategy Pattern: Pluggable eviction strategies (LRU, LFU, FIFO)
 *  - Singleton Pattern: Centralized cache manager
 */

import java.util.*;

// --- [DESIGN PATTERN: Strategy] Cache eviction strategy ---
interface EvictionStrategy<K> {
    void onKeyAccessed(K key);
    K evictKey();
}

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

    @Override
    public void onKeyAccessed(K key) {
        accessOrder.remove(key);
        accessOrder.add(key);
    }

    @Override
    public K evictKey() {
        Iterator<K> it = accessOrder.iterator();
        if (it.hasNext()) {
            K oldest = it.next();
            it.remove();
            return oldest;
        }
        return null;
    }
}

// --- [DESIGN PATTERN: Proxy] Read-through database proxy ---
interface DataStore<K, V> {
    V get(K key);
}

class DatabaseStore implements DataStore<String, String> {
    @Override
    public String get(String key) {
        System.out.println("💾 [Database I/O] Costly DB lookup for key: " + key);
        return "Record_For_" + key;
    }
}

class CacheProxy implements DataStore<String, String> {
    private final DataStore<String, String> db;
    private final int capacity;
    private final Map<String, String> storage = new HashMap<>();
    private final EvictionStrategy<String> evictionStrategy;

    public CacheProxy(DataStore<String, String> db, int capacity, EvictionStrategy<String> evictionStrategy) {
        this.db = db;
        this.capacity = capacity;
        this.evictionStrategy = evictionStrategy;
    }

    @Override
    public synchronized String get(String key) {
        if (storage.containsKey(key)) {
            System.out.println("⚡ [Cache HIT] key: " + key);
            evictionStrategy.onKeyAccessed(key);
            return storage.get(key);
        }

        System.out.println("❌ [Cache MISS] key: " + key);
        String val = db.get(key);
        put(key, val);
        return val;
    }

    public synchronized void put(String key, String value) {
        if (storage.size() >= capacity && !storage.containsKey(key)) {
            String evicted = evictionStrategy.evictKey();
            if (evicted != null) {
                storage.remove(evicted);
                System.out.println("🗑️ [Eviction] Evicted least recently used key: " + evicted);
            }
        }
        storage.put(key, value);
        evictionStrategy.onKeyAccessed(key);
    }
}

public class Main {
    public static void main(String[] args) {
        DatabaseStore db = new DatabaseStore();
        CacheProxy cache = new CacheProxy(db, 2, new LRUEvictionStrategy<>());

        System.out.println("--- Starting Cache Simulation ---");
        cache.get("user:1"); // MISS
        cache.get("user:1"); // HIT
        cache.get("user:2"); // MISS
        cache.get("user:3"); // MISS -> causes eviction of user:1 or user:2
        cache.get("user:1"); // MISS (re-fetched from DB)
    }
}