Skip to content

Kata: memoize

Key on the exact arguments, store exactly what came back, and give back nothing that failed.

Predict first

Before you read: predict

ExamHall's Scoring service memoizes its rubric lookups as rubricFor = memoize(loadRubric). loadRubric(examId, version) returns one rubric object, and a missing version means the published one. The team's kata answer:

A grader calls rubricFor('ECON-201') and then rubricFor('ECON-201', 3).

What happens?

Code for this question
export function memoize(fn) {  const cache = new Map();  return function (...args) {    let node = cache;    for (const arg of args.slice(0, -1)) {      if (!node.has(arg)) node.set(arg, new Map());      node = node.get(arg);    }    const last = args[args.length - 1];    if (node.has(last)) return node.get(last);    const result = fn.apply(this, args);    node.set(last, result);    return result;  };}
Pick one

The 30-second version

A kata memoize keeps one cache per memoize() call: a trie of Maps, walked by the argument count first and then by each argument, so types, NaN and object identity survive and a result never sits on a longer call's path. It checks with has, calls fn.apply(this, args), stores what returns, and deletes a promise's entry when it rejects.

Key on the exact arguments, store exactly what came back, and give back nothing that failed.

How it works

Step 1 of 13

Ask, then say the contract. Several arguments? A resolver? Async results? Then: equal arguments run fn once, this is forwarded, and failures are not cached.

Analogy: A locksmith's wall of hooks (trades)

A locksmith hangs every key she cuts, so a repeat order is handed over without cutting.

At the key-cutting counter In the kata
a wall per counter, never shared with the shop next door one root Map per memoize() call (step 2)
a row per number of pins, then a peg per pin depth, the key on the last peg a trie walked by [args.length, ...args] (steps 4–5)
no row per pin count: a five-pin key hangs where a six-pin order expects a sixth peg a result on a longer call's path (step 5)
an order "for this padlock" hangs under that padlock's own tag, not its description objects keyed by identity (steps 3–4)
the ticket goes on the hook the moment she starts cutting the promise stored before it settles (step 9)
a blank that cracks in the machine comes off its hook a rejected promise's entry deleted (step 10)
a wall that only fills the leak (step 12)

In real terms: memoize is a lookup in a tree of Maps, built from the exact arguments, in front of one function.

Breaks down when: a key on a hook can be copied for the next customer, while a cached result is the very same object handed to every caller; a caller who changes it changes it for all.

Contrasting case: a locksmith who files keys by a written description: two padlocks described alike share one hook, and one padlock described in another order gets a second. That is a JSON.stringify key.

Examples, run for real

4.1 ExamHall EXH-2840: a cached rejection fails every later grading

Scoring memoized rubricFor = memoize(fetchRubric) with a draft that stored the promise and never deleted it (EXH-2840). The rubric service failed once, on grading 1, and was back for gradings 2–6. MR !3140 replaced the draft with the reference and added the test list to the repository. The list also runs on three common mutants: M1 keys on args.join(','), M2 never deletes a rejection, and M3 checks the hit with get.

4.1 reference.mjs                      17 of 17 pass (14 core, 3 ladder)
4.1 M1, args.join(',') as the key      14 of 17 pass · red: C3 "the number 1 and the string "1" are two keys", C4 "two equal-looking objects are two keys, and one object is one key", C6 "f() and f(undefined) are two calls"
4.1 M2, no eviction                    16 of 17 pass · red: C12 "after a rejection, the next call runs fn again"
4.1 M3, get() for the hit check        16 of 17 pass · red: C8 "a cached undefined is a hit"
4.1 EXH-2840 the M2 draft in Scoring, the service down for grading 1 only, 6 gradings: 6 failed, 1 rubric load
4.1 MR !3140 the reference in Scoring, the same 6 gradings: 1 failed, 2 rubric loads

M2 keeps the rejected promise (step 10), so one outage fails every grading until the process restarts; the reference deletes it and grading 2 loads again. M1 merges keys that differ in type (step 3), and M3 reruns fn for a cached undefined (step 6).

4.2 Ticketing: one seat, one entry

The box office memoizes quoteFor = memoize(quoteSeat) and quotes when a fan clicks a seat. One handler passes the seat map's own seat object; another builds { row, num } on each click:

4.2 the seat map's own seat object, 3 clicks on A12: 1 quote · 1 entry
4.2 a new { row, num } per click, 3 clicks on A12: 3 quotes · 3 entries

The map's object finds its own entry, while a new object per click is a new key every time (step 4), and the cache grows by one entry per click (step 12).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 8

A box office prices seats per venue, with this kata answer of memoize:

The page builds arena = new Venue('arena', tariffs) and hall = new Venue('hall', tariffs), then calls arena.price('A1') and hall.price('A1').

Which lookups reach the tariff service, and what does the hall's call return?

Code for this question
export function memoize(fn) {  const cache = new Map();  return function (...args) {    const path = [args.length, ...args];    let node = cache;    for (const part of path.slice(0, -1)) {      if (!node.has(part)) node.set(part, new Map());      node = node.get(part);    }    const last = path[path.length - 1];    if (node.has(last)) return node.get(last);    const result = fn.apply(this, args);    node.set(last, result);    return result;  };} export class Venue {  constructor(id, tariffs) {    this.id = id;    this.tariffs = tariffs;  }}Venue.prototype.price = memoize(function (seat) {  return this.tariffs.lookup(this.id, seat);});
Pick one

Question 2 of 8

ExamHall's Scoring memoizes rubricFor = memoize(fetchRubric), and fetchRubric returns a promise. A candidate's kata answer is complete apart from one line:

The team's list has 4 tests whose fn returns a promise, and only those are run.

Which line passes all 4?

Code for this question
export function memoize(fn) {  const cache = new Map();  return function (...args) {    const path = [args.length, ...args];    let node = cache;    for (const part of path.slice(0, -1)) {      if (!node.has(part)) node.set(part, new Map());      node = node.get(part);    }    const last = path[path.length - 1];    if (node.has(last)) return node.get(last);    const result = fn.apply(this, args);    node.set(last, result);    if (typeof result?.then === 'function') {      /* the line */    }    return result;  };}
Pick one

Question 3 of 8

A chat app counts the ways to lay out n reaction chips in rows of one or two chips. memoize is the kata's reference answer:

The app calls fastLayouts(10) and then fastLayouts(11).

How many times does the body of layouts run during the second call?

Code for this question
import { memoize } from './memoize.mjs'; function layouts(n) {  return n < 2 ? 1 : layouts(n - 1) + layouts(n - 2);}export const fastLayouts = memoize(layouts);
Pick one

Question 4 of 8

A shop memoizes shipping quotes per zone as quoteFor = memoize(quoteShipping), with a draft that keys one flat Map on a string:

Under this draft, which two calls share one cache entry, so that quoteShipping runs once for the pair?

Code for this question
export function memoize(fn) {  const cache = new Map();  return function (...args) {    const key = JSON.stringify(args);    if (cache.has(key)) return cache.get(key);    const result = fn.apply(this, args);    cache.set(key, result);    return result;  };}
Pick one

Question 5 of 8

the interviewer says: "Implement memoize(fn) for a function of several arguments. Equal arguments, compared the way a Map compares keys, run fn once; forward this; and don't cache a failure, a rejected promise included." You have 15 minutes and a plain editor, and Scoring's rubric lookups are the use case.

Before you type: say your test list out loud.

Name at least eight tests, each with its calls and what it expects.

Question 6 of 8

the interviewer says: "Implement memoize(fn) for a function of several arguments. Equal arguments, compared the way a Map compares keys, run fn once; forward this; and don't cache a failure, a rejected promise included." You have 15 minutes and a plain editor, and Scoring's rubric lookups are the use case.

Now write it, in 15 minutes, from a blank file that exports memoize.

Question 7 of 8

A video player memoizes qualityFor(mbps), which builds its quality ladder from a bandwidth estimate. The estimate is NaN before the first sample and can be -0 after a reset. A candidate's draft keeps its entries in an array:

The player calls qualityFor(NaN), qualityFor(NaN), qualityFor(0) and qualityFor(-0). A Map-keyed memoize builds the ladder twice for these four calls; this draft builds it three times.

Which comparison in place of a === args[i] gives two builds?

Code for this question
export function memoize(fn) {  const entries = [];  return function (...args) {    const hit = entries.find((e) =>      e.args.length === args.length && e.args.every((a, i) => a === args[i]));    if (hit) return hit.result;    const result = fn.apply(this, args);    entries.push({ args, result });    return result;  };}
Pick one

Question 8 of 8

A payments ledger memoizes balance reads with the kata's reference memoize and a resolver:

Account ids are strings of digits, and day is a number. A statement page calls balanceOn('12', 3), balanceOn('1', 23), balanceOn('13', 2) and balanceOn('12', 3) again, and account 1's day-23 balance shows account 12's day-3 balance.

Which resolver makes readBalance run for each of the first three calls and answers the fourth from the cache?

Code for this question
export const balanceOn = memoize(readBalance, (account, day) => account + day);
Pick one

Go deeper

Pro tips
  1. Name the leak before you are asked: a Map cache holds its keys and results for as long as the memoized function lives, so offer a size cap (LRU) or WeakMap levels for object keys. When: after the core tests pass, when the interviewer turns to production (step 12). Verify (RAN, tips.mjs): the same 1,000 quote clicks with a new object each time leave 1,000 entries in the trie; clearing cache leaves 0. (The curriculum row's tip, [SELF].)
    tip 1 1000 clicks, a new seat object each: 1000 entries · after quoteFor.cache.clear(): 0
  2. Run your own answer against the list before you call it done: KATA_IMPL=./my-memoize.mjs node --test kata-core.test.mjs. When: practising the kata at home, against the same tests the reference passes (step 13). Verify (RAN, tips.mjs):
    tip 2 KATA_IMPL=./reference.mjs: exit 0 · tests 14 · pass 14 · fail 0
    tip 2 KATA_IMPL=<temp>/m1-memoize.mjs (args.join(',') as the key): exit 1 · tests 14 · pass 11 · fail 3 · red: C3, C4, C6
Source

Vendor and guide pages change, so their claims are labelled as of 2026-10-04; every behaviour this lesson grades is executed on Node 24.19.0.

  • Lodash 4.17.15 documentation, _.memoize (the curriculum row's first source), https://lodash.com/docs/4.17.15#memoize. lodash.com refused connections on 2026-10-04 (curl exit 28, at 08:10:15Z and on the retry), so the page was read from the unmodified Internet Archive capture (id_, Memento-Datetime 2025-12-05T10:14:29Z) of that URL, as _fix-brief-r1.md ruling 1 accepts for a named primary page (lodash-memoize.txt, header lines Via, Memento-Datetime and Raw-SHA256). It says a resolver "determines the cache key", that "By default, the first argument provided to the memoized function is used as the map cache key", that func "is invoked with the this binding of the memoized function", and that "The cache is exposed as the cache property" (steps 7–8, 12). Lodash keys no item; this lesson's reference is its own code.
  • MDN, Map (the row's second source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map (mdn-map.txt): "Value equality is based on the SameValueZero algorithm", "NaN is considered the same as NaN", and "for object keys, equality is based on object identity" (step 4). As of 2026-10-04.
  • MDN, WeakMap (the row's third source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap (mdn-weakmap.txt): keys "must be objects or non-registered symbols", and a key's presence "does not prevent the object from being garbage collected" (step 12, pro tip 1). As of 2026-10-04.
  • MDN, Array.prototype.includes(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes (mdn-includes.txt): it "compares searchElement to elements of the array using the SameValueZero algorithm". As of 2026-10-04.
  • Front End Interview Handbook, JavaScript coding, https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (feih-javascript-coding.txt): candidates take "around 10-15 minutes for a basic question" (the time box, step 1), and it advises "writing test cases" (step 13). As of 2026-10-04.

Checked against the tools and docs as of 2026-10-04.