Don't ask the database twice
Reading memory takes nanoseconds; a database query takes milliseconds. A cache keeps recent answers close by. It must stay small, so it evicts the LRU (least recently used) entry when full — and entries expire after a TTL so stale data doesn't live forever.
LRU with a Map
function createCache({ capacity, ttl = Infinity, now = () => Date.now() }) {
const map = new Map(); // a Map remembers insertion order: oldest first
let hits = 0;
let misses = 0;
return {
get(key) {
const entry = map.get(key);
if (!entry || now() >= entry.expires) {
if (entry) map.delete(key); // expired
misses++;
return undefined;
}
map.delete(key); // move to the newest end
map.set(key, entry);
hits++;
return entry.value;
},
set(key, value) {
map.delete(key);
map.set(key, { value, expires: now() + ttl });
if (map.size > capacity) map.delete(map.keys().next().value); // evict least recently used
},
async getOrLoad(key, loader) {
const cached = this.get(key);
if (cached !== undefined) return cached;
const value = await loader(key); // cache-aside: miss → load → store
this.set(key, value);
return value;
},
stats() {
const total = hits + misses;
return { hits, misses, hitRate: total ? hits / total : 0 };
},
size: () => map.size,
};
}