Implement least-recently-used cache
Build least-recently-used cache. The interviewer expects a small, reusable utility with clear behavior under repeated calls and invalid inputs.
Answer Strategy
For least-recently-used cache, start by stating the public contract before writing code: argument shape, return shape, mutation rules, error behavior, and whether work is synchronous, timed, cached, or cancellable.
A senior solution uses boring names for hidden state. If the function stores a timer, cache entry, listener, or in-flight promise, say who owns that state and how it is cleaned up.
After the baseline passes, harden the edge cases: empty input, repeated calls, invalid values, thrown callbacks, stable ordering, and memory lifetime. The reference below is written to be narrated line by line.
Reference Implementation: LRU Cache
An LRU cache is easiest to narrate with Map insertion order: delete and reinsert on every successful get.
class LruCache<K, V> {
private values = new Map<K, V>();
constructor(private readonly capacity: number) {}
get(key: K): V | undefined {
if (!this.values.has(key)) return undefined;
const value = this.values.get(key)!;
this.values.delete(key);
this.values.set(key, value);
return value;
}
set(key: K, value: V) {
if (this.values.has(key)) {
this.values.delete(key);
}
this.values.set(key, value);
while (this.values.size > this.capacity) {
const oldest = this.values.keys().next().value;
this.values.delete(oldest);
}
}
keys() {
return [...this.values.keys()];
}
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
class LruCache<K, V> {
private values = new Map<K, V>();
constructor(private readonly capacity: number) {}
get(key: K): V | undefined {
if (!this.values.has(key)) return undefined;
const value = this.values.get(key)!;
this.values.delete(key);
this.values.set(key, value);
return value;
}
set(key: K, value: V) {
if (this.values.has(key)) {
this.values.delete(key);
}
this.values.set(key, value);
while (this.values.size > this.capacity) {
const oldest = this.values.keys().next().value;
this.values.delete(oldest);
}
}
keys() {
return [...this.values.keys()];
}
}
function createLruCache(capacity: number) {
return new LruCache<string, number>(capacity);
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
test('LruCache evicts the least recently used key', () => {
const cache = new LruCache<string, number>(2);
cache.set('a', 1);
cache.set('b', 2);
expect(cache.get('a')).toBe(1);
cache.set('c', 3);
expect(cache.get('b')).toBeUndefined();
expect(cache.keys()).toEqual(['a', 'c']);
});Interviewer Signal
Tests whether you can turn a familiar utility into a precise contract instead of coding only the happy path.
Constraints
- Define the function signature before coding.
- Do not rely on global mutable state unless it is part of the returned closure.
- Explain time and space cost for the common path.
Model Answer Shape
- Write the smallest public contract first.
- Cover empty input, repeated calls, thrown errors, and cleanup behavior.
- Keep implementation readable enough to narrate under interview pressure.
Tradeoffs
- A compact implementation is attractive, but explicit state names are easier to debug live.
- Supporting every possible input can distract from the core contract; state the scope before coding.
Edge Cases
- No arguments or undefined callbacks.
- Synchronous throw inside the wrapped function.
- Repeated calls before the previous result settles.
Testing And Proof
- Happy path with representative inputs.
- Boundary input and repeated invocation.
- Cleanup or cancellation if timers or promises are involved.
Follow-Ups
- How would you expose cancellation?
- How would the API change for React usage?