Memoize and Cache Layer

Hard100% Free~30 mins#memoize#caching#closures#performance#lru-cache#weakmap#higher-order-functions
Key Learning Objectives
✓

Differentiate pure function memoization from general HTTP and database caching.

✓

Implement closure-based function memoization supporting multiple arguments and custom key resolvers.

✓

Understand argument serialization traps: why JSON.stringify distinguishes argument order and fails on circular objects.

✓

Implement memory-leak resilient caching using WeakMap and LRU (Least Recently Used) capacity bounds.

✓

Identify critical limitations: why functions with side effects, non-deterministic outputs, or async rejections cannot be safely memoized.

The Interview Problem

What is logged to the console when the following memoized functions are invoked, and how do default JSON argument serialization versus custom key resolvers determine cache hit rates?

1function memoize(fn, resolver) {
2 const cache = new Map();
3 return function (...args) {
4 const key = resolver ? resolver(...args) : JSON.stringify(args);
5 if (cache.has(key)) {
6 return { value: cache.get(key), cached: true };
7 }
8 const result = fn.apply(this, args);
9 cache.set(key, result);
10 return { value: result, cached: false };
11 };
12}
13
14let calls = 0;
15const add = (a, b) => {
16 calls++;
17 return a + b;
18};
19
20const defaultMem = memoize(add);
21const customMem = memoize(add, (a, b) => [a, b].sort().join(','));
22
23const r1 = defaultMem(1, 2);
24const r2 = defaultMem(1, 2);
25const r3 = defaultMem(2, 1);
26const r4 = customMem(1, 2);
27const r5 = customMem(2, 1);
28
29console.log(calls, r1.cached, r2.cached, r3.cached, r5.cached);
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

3 false true false true

2 false true true true

4 false true false false

5 false false false false

V8 Engine Execution Trace
Step 1 of 7 (Line 1)

Initializes global environment and allocates memoized wrapper functions.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
calls:0
Console Stream
> [empty]

Deep Technical Breakdown

Anatomy of Production-Grade Memoization

Memoization is an optimization technique that caches the return values of pure functions based on their input arguments:

  1. Closure Scope: The cache (typically a Map) is encapsulated within the outer function's lexical scope, preserved across calls.
  2. Context Preservation: Always invoke the target function with fn.apply(this, args) to ensure object methods retain their correct this binding.
  3. Key Generation Pitfalls:
    • JSON.stringify(args) works for simple primitives and JSON-serializable structures, but fails on circular references (TypeError: Converting circular structure to JSON) and ignores functions or undefined object properties.
    • Passing object arguments { a: 1, b: 2 } and { b: 2, a: 1 } produces different strings if key ordering varies, causing false cache misses.

The Memory Leak Problem & LRU Caching

Unbounded memoization caches retain every input and result forever in heap memory. In long-running single-page applications, this causes silent memory exhaustion. The Senior Solution: Implement a Bounded LRU (Least Recently Used) Cache:

javascript
function memoizeLRU(fn, capacity = 100) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      const val = cache.get(key);
      cache.delete(key);
      cache.set(key, val); // Move to most recently used
      return val;
    }
    const result = fn.apply(this, args);
    if (cache.size >= capacity) {
      const oldestKey = cache.keys().next().value;
      cache.delete(oldestKey); // Evict oldest
    }
    cache.set(key, result);
    return result;
  };
}

When NOT to Memoize

  • Impure Functions: Functions reading system state (Date.now(), Math.random(), DOM elements, or global variables).
  • Asynchronous Functions with Failures: If a memoized promise rejects, naive caching caches the rejected promise forever, preventing retry recovery.
Common Traps & Mistakes

Assuming `JSON.stringify` handles object references by pointer identity. It serializes structural values, risking collisions and failing on circular objects.

Forgetting to preserve `this` using `.apply(this, args)`. When memoizing class or object methods, losing `this` causes runtime errors.

Memoizing asynchronous functions without handling rejected promises, resulting in permanently cached rejected states.

Failing to bound cache capacity, leading to unbounded memory leaks in enterprise applications.

FAANG Follow-Up Probes
Probe #1

How would you use WeakMap to memoize single-argument functions where the argument is always an object, avoiding memory leaks?

Probe #2

How can you handle asynchronous memoization where duplicate concurrent calls share a single in-flight Promise (request coalescing)?

Probe #3

What is the difference between React.memo (component shallow comparison) and useMemo (dependency array caching)?