Stack QuestJoin the beta

5 programs you build yourself, and keep.

A project is not a video to watch or a repository to clone. Each one is a handful of functions you write, in order, where every function builds on the one before it and is checked by tests that run on your phone. Finish the last step and you have a working thing.

Projects
5
Steps
25
Tests
99
Minutes
≈135start to finish, all five
System design

Build a cache that forgets

Five steps that turn a plain object into a cache with expiry, a size cap and least-recently-used eviction.

What you end up with

A cache a real service could use: entries that expire, a hard size limit, least-recently-used eviction, and hit and miss counts that show whether it is earning its keep.

  • 5 steps
  • 20 tests
  • 25 minutes
  • javascript
  1. 1

    Remember a valuecacheSet()

    Complete cacheSet(cache, key, value, now). cache is { ttlMs, maxEntries, entries }. Store { value, storedAt: now, usedAt: now } under key in cache.entries, replacing anything already there, and return the cache.

  2. 2

    Stop serving stale answerscacheGet()

    Complete cacheGet(cache, key, now). Return null if key is not in cache.entries, or if now - storedAt is at least cache.ttlMs — in that case delete the entry first. Otherwise set the entry’s usedAt to now and return its value.

  3. 3

    Cap how much it holdscachePut()

    Complete cachePut(cache, key, value, now). Store the entry with cacheSet, then while cache.entries holds more than cache.maxEntries keys, delete the entry with the smallest storedAt. Return the cache.

  4. 4

    Evict the one nobody wantsevictLru()

    Complete evictLru(cache). While cache.entries holds more than cache.maxEntries keys, delete the entry with the smallest usedAt; if two tie, the key added first is the one that goes. Return the cache. Then change cachePut to call evictLru instead of dropping the oldest entry.

  5. 5

    Count the hits and missesrunCache()

    Complete runCache(cache, ops). Each op is ["set", key, value, now] or ["get", key, now]; run sets through cachePut and gets through cacheGet, collecting each get’s return value in order. Return { results, hits, misses, hitRate, keys }, where a get returning null is a miss, hitRate is hits divided by the number of gets rounded to two decimals (0 when there were no gets), and keys is Object.keys(cache.entries).

System design

Build a rate limiter

Five steps from a fixed window that leaks bursts to a per-caller token bucket a service could run on.

What you end up with

A working token-bucket rate limiter: buckets that refill over time, one per caller, and a decision function that answers allowed, how many left, and when to retry.

  • 5 steps
  • 20 tests
  • 30 minutes
  • javascript
  1. 1

    Count hits in a windowfixedWindow()

    Complete fixedWindow(state, now, limit, windowMs). state is { windowStart, count } and now is a timestamp in milliseconds. The window now belongs to starts at Math.floor(now / windowMs) * windowMs; if that is not state.windowStart, the count restarts at 0. Return { allowed, windowStart, count }: when the count so far is below limit, allowed is true and count goes up by one, otherwise allowed is false and count is unchanged.

  2. 2

    Watch a burst slip throughreplayFixed()

    Complete replayFixed(times, limit, windowMs). times is an array of arrival timestamps in milliseconds, in order. Start from the state { windowStart: -1, count: 0 } — no real window starts before 0 — and feed each timestamp through fixedWindow, carrying the windowStart and count it returns into the next call. Return { allowed, denied }: how many arrivals were allowed and how many were refused. Call fixedWindow rather than repeating its logic.

  3. 3

    Refill a bucket over timerefill()

    Complete refill(bucket, now, capacity, refillMs). bucket is { tokens, last } — tokens on hand, and the timestamp they were last counted at. One whole token arrives every refillMs milliseconds, so the bucket gains Math.floor((now - bucket.last) / refillMs) tokens, capped at capacity. Move last forward by exactly that many refill intervals, not to now. Return the new { tokens, last }; if that gain is zero or less, return the same tokens and last you were given.

  4. 4

    One bucket per callerbucketFor()

    Complete bucketFor(buckets, key, now, capacity, refillMs). buckets maps a caller key to that caller's { tokens, last }. Return the bucket for key, refilled up to now with refill. A key that is not in buckets yet starts full, as { tokens: capacity, last: now }. Return the bucket only, and leave buckets itself alone.

  5. 5

    Allowed, or come back latercheck()

    Complete check(buckets, key, now, capacity, refillMs). Get the caller's refilled bucket from bucketFor. If it holds at least one token, spend one and return { allowed: true, remaining, retryAfterMs: 0 }, where remaining is what is left after spending. If it holds none, return { allowed: false, remaining: 0, retryAfterMs }, where retryAfterMs is bucket.last + refillMs - now: the wait until the next token lands. Either way, store the bucket you ended up with back into buckets under key so the next call sees it.

Data Structures & Algorithms

Build a text search index

Five functions that turn a pile of documents into a search box that ranks what it finds.

What you end up with

A working search index: a tokenizer, an inverted index, term lookup, frequency ranking, and multi-word queries that return the best document first.

  • 5 steps
  • 20 tests
  • 30 minutes
  • javascript
  1. 1

    Break the text into wordstokenize()

    Complete tokenize(text). Return an array of the words in text: lowercased, split on every run of characters that is not a letter or digit, with empty strings dropped. Text with no letters or digits returns [].

  2. 2

    Turn the documents inside outbuildIndex()

    Complete buildIndex(docs). docs is an array of { id, text }. Return an object mapping each token to an object mapping document id to the number of times that token appears in that document. Use tokenize. An empty docs returns {}.

  3. 3

    Look a word uplookup()

    Complete lookup(index, term). Run term through tokenize and take the first token. Return the document ids listed under that token, in the order they appear in the index entry. Return [] if the term has no token, or the index has no entry for it.

  4. 4

    Put the best result firstrank()

    Complete rank(index, term). Return an array of { id, score } for every document lookup finds, where score is that document’s count for the term. Sort by score descending, breaking ties by id ascending. No hits returns []. Use lookup rather than reading the index twice.

  5. 5

    Answer a real querysearch()

    Complete search(docs, query). Tokenize query, build the index from docs, and return { id, score } for every document containing every one of the query’s tokens, where score is the sum of that document’s counts for them. Sort by score descending, ties by id ascending. A query with no tokens returns []. Use the functions you already wrote.

JavaScript

Build a receipt printer

Five functions that add up to a receipt you could hand someone.

What you end up with

A working receipt printer: line totals, a subtotal, tax, money formatting, and the function that prints the lot.

  • 5 steps
  • 19 tests
  • 25 minutes
  • javascript
  1. 1

    One line of the receiptlineTotal()

    Complete lineTotal(item). item is { name, price, qty }. Return price × qty rounded to the nearest cent, as a number.

  2. 2

    Add the lines upsubtotal()

    Complete subtotal(items). items is an array of { name, price, qty }. Return the sum of every line total, rounded to the nearest cent. Use lineTotal rather than repeating its maths.

  3. 3

    Charge the taxwithTax()

    Complete withTax(amount, rate). rate is a fraction, so 8% arrives as 0.08. Return the amount with tax added, rounded to the nearest cent.

  4. 4

    Make it look like moneymoney()

    Complete money(amount). Return the amount as a string with a leading $ and exactly two decimal places.

  5. 5

    Print the whole thingprintReceipt()

    Complete printReceipt(items, rate). Return an array of strings: one line per item as NAME xQTY $AMOUNT, then Subtotal $X, Tax $X, and Total $X. Tax is the difference between the subtotal and the taxed total. Use the functions you already wrote.

Testing & craft

Build a test runner

Five functions that grow into the runner every testing framework is underneath.

What you end up with

A working test runner: deep equality, assertions that answer instead of throwing, a whole suite counted, and a failure report you can read.

  • 5 steps
  • 20 tests
  • 25 minutes
  • javascript
  1. 1

    Tell whether two values matchdeepEqual()

    Complete deepEqual(a, b). Return true when the two values are equal: identical primitives, arrays of the same length whose items are all deep-equal, or objects with the same set of keys whose values are all deep-equal. Everything else is false, including an array compared with a plain object.

  2. 2

    Answer instead of throwingcheck()

    Complete check(name, actual, expected). Return { name, pass, actual, expected } — the same four keys whether it passed or failed — where pass is true only when actual and expected are deep-equal. Use deepEqual; never throw.

  3. 3

    Run one test caserunCase()

    Complete runCase(testCase). A case is { name, actual, expected }, or { name, error } when the code under test threw instead of returning. If the case has an error property, return { name, pass: false, error } and compare nothing; otherwise return the result of check.

  4. 4

    Run the whole suiterunSuite()

    Complete runSuite(cases). Run each case with runCase, in order, and return { total, passed, failed, results }total is how many cases ran, passed how many have pass: true, failed the rest, and results the array of case results in the original order.

  5. 5

    Say what failed and whyreport()

    Complete report(cases). Run them with runSuite, then return an array of strings: for each failing result, in order, FAIL <name>, then either the single line threw: <error> when the case errored, or the two lines expected: <JSON> and actual: <JSON>. Passing results add no lines. End the array with one summary line, <passed> passed, <failed> failed, <total> total. Detail lines start with two spaces, and values are rendered with JSON.stringify.

How the grading works

Your function is run against the step's test cases by a JavaScript engine inside the app, on your device. Passing is a matter of the tests passing — no model reads your code, no answer is graded by judgement, and nothing you write leaves the phone. Every step's tests are in the same public content bundle the rest of this site is built from.