Volume 2

Q122: Memoization, Cache Keys and Invalidation

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

This question checks whether you understand memoization as more than storing function results. Interviewers want pure functions, cache keys, object identity, memory growth, invalidation, stale results, async memoization, and when memoization makes performance worse.

Senior answers should connect memoization to React rendering, selectors, expensive calculations, and production cache risk.

Short Interview Answer

Memoization caches the result of a function call so repeated calls with the same inputs can reuse the result. It works best for pure, expensive functions with stable inputs. The hard parts are choosing correct cache keys, avoiding unbounded memory growth, and invalidating stale results. For object inputs, identity-based keys may miss equivalent values or retain objects. Memoization is useful when repeated computation cost is higher than cache overhead.

Detailed Interview Answer

Memoization is a performance technique.

function memoize(fn) {
  const cache = new Map();

  return function memoized(arg) {
    if (cache.has(arg)) {
      return cache.get(arg);
    }

    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

This simple version works for one argument where Map key identity is good enough.

Pure Function Requirement

Memoization is safest for pure functions.

function square(value) {
  return value * value;
}

The same input always produces the same output.

If a function depends on time, random values, permissions, global state, or network data, memoization can return wrong results.

Cache Keys

The cache key must represent every input that affects the result.

function getPrice(productId, currency) {
  return prices[productId][currency];
}

Caching only by productId would be wrong because currency changes the result.

Multiple Arguments

A common approach is a composite key.

function memoizeTwo(fn) {
  const cache = new Map();

  return (a, b) => {
    const key = `${a}:${b}`;

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn(a, b);
    cache.set(key, result);
    return result;
  };
}

This is easy but can break if values contain the separator or are not strings.

Object Identity

Objects are compared by reference in Map.

const cache = new Map();

cache.set({ id: 1 }, "cached");
cache.get({ id: 1 }); // undefined

The two object literals have the same shape and value but different identities.

Structural Keys

Some memoizers serialize inputs.

const key = JSON.stringify(args);

This has limitations:

property order can matter
JSON-unsafe values are lost
large keys are expensive
functions and symbols do not serialize
circular references fail

Structural keys are not free.

WeakMap Memoization

WeakMap can memoize by object identity without keeping object keys alive forever.

function memoizeByObject(fn) {
  const cache = new WeakMap();

  return (object) => {
    if (cache.has(object)) {
      return cache.get(object);
    }

    const result = fn(object);
    cache.set(object, result);
    return result;
  };
}

This is useful when the input is an object and object lifetime should control cache lifetime.

Invalidation

Invalidation is deciding when cached data is no longer valid.

Options include:

manual delete
time-to-live
least recently used eviction
dependency version key
cache clear on mutation
bounded cache size

Without invalidation, memoization can return stale values or grow forever.

Async Memoization

Async memoization often caches promises.

const userRequests = new Map();

function loadUser(id) {
  if (!userRequests.has(id)) {
    userRequests.set(id, fetchUser(id));
  }

  return userRequests.get(id);
}

This deduplicates concurrent requests. But rejected promises and stale data need handling.

Rejected Promise Problem

If a cached promise rejects, future calls may receive the same rejection forever.

userRequests.set(id, fetchUser(id).catch((error) => {
  userRequests.delete(id);
  throw error;
}));

Deleting failed entries lets callers retry.

React Connection

In React, memoization tools such as useMemo, useCallback, selector memoization, and component memoization reduce repeated work only when dependencies are stable and the avoided work matters.

Memoization can add complexity without benefit if the computation is cheap or inputs change every render.

Common Mistakes

A common mistake is memoizing impure functions.

Another mistake is creating unbounded caches in long-lived modules.

Another mistake is using JSON string keys without considering data shape and cost.

Senior Trade-Offs

Memoization trades memory and complexity for CPU or network savings.

Use it when:

the function is expensive
inputs repeat
outputs are safe to reuse
cache keys are correct
memory growth is bounded
invalidation is defined

Code or Design Example

function createLruMemo(fn, limit = 50) {
  const cache = new Map();

  return (key) => {
    if (cache.has(key)) {
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    }

    const result = fn(key);
    cache.set(key, result);

    if (cache.size > limit) {
      cache.delete(cache.keys().next().value);
    }

    return result;
  };
}

This bounds memory while keeping recently used values.

Debugging Workflow

When memoization misbehaves:

verify the function is pure enough
inspect cache key completeness
check object identity assumptions
check stale invalidation
check rejected async cache entries
measure cache hit rate
measure memory growth
remove memoization if overhead exceeds benefit

Interview Framing

Define memoization, then quickly move to cache keys and invalidation. That is where senior-level reasoning lives.

Revision Notes

Memoization is safe only when repeated inputs truly mean repeated results. The cache key and invalidation policy are the design.

Key Takeaways

Memoization is not magic performance dust. It is a cache with all the usual cache trade-offs.

Follow-Up Questions

  1. What is memoization?
  2. Why should memoized functions be pure?
  3. What makes a good cache key?
  4. How are object keys compared in Map?
  5. When would WeakMap help?
  6. What is invalidation?
  7. Why can async memoization cache failures?
  8. What is an LRU cache?
  9. When does memoization hurt performance?
  10. How do React memoization tools relate?

How I Would Answer This In A Real Interview

I would define memoization as caching function results, then say the hard parts are cache keys, invalidation, memory growth, and purity. I would show an object-key or async example to prove I understand the production risks.