Skip to main content

Founding engineer coding interview questions

11 questions on coding for founding engineer candidates. Expect prompts such as “Implement an LRU cache in TypeScript” and “Implement debounce in TypeScript with a leading edge option”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Implement a Least Recently Used cache class in TypeScript with get(key) and put(key, value) methods, both running in O(1) time. The cache has a fixed capacity and evicts the least recently used entry when full.

Sample answer outline

Use a doubly-linked list for O(1) move-to-front and a Map for O(1) key lookup. The Map stores references to list nodes. On get, move the node to the head. On put, insert at head and evict the tail if over capacity. A strong answer writes clean TypeScript generics and handles the edge case of updating an existing key.

Reference implementation (typescript)

TypeScript
class LRUCache<K, V> {
  private capacity: number;
  // TODO: add your data structures here

  constructor(capacity: number) {
    this.capacity = capacity;
  }

  get(key: K): V | -1 {
    // implement
  }

  put(key: K, value: V): void {
    // implement
  }
}

Expect these follow-ups

  • Why does a Map plus a doubly-linked list give you O(1) rather than O(n) for eviction?
  • How would you make this cache thread-safe in a multi-threaded environment?
data-structureslinked-listhash-maptypescriptlru

As asked

Write a debounce function in TypeScript that accepts a function, a delay in milliseconds, and an optional options object with a leading flag. When leading is true, the function fires on the first call immediately and then ignores subsequent calls until the delay has elapsed.

Sample answer outline

Correct solution uses a closure over a timer variable, clears the timer on each call, and conditionally invokes the function immediately if leading is true and no timer is running. Candidate should handle the return type generically, preserve the this context, and pass arguments correctly to the underlying function.

Reference implementation (typescript)

TypeScript
function debounce<T extends (...args: unknown[]) => unknown>(
  fn: T,
  delay: number,
  options?: { leading?: boolean }
): (...args: Parameters<T>) => void {
  // implement here
}

Expect these follow-ups

  • What is the difference between debounce and throttle, and when would you use throttle instead?
  • How would you write a test for the leading flag behavior without relying on real timers?
javascripttypescriptclosuresasyncutility-functions

As asked

Write a TypeScript function flattenObject(obj) that converts a nested object into a flat object where each key is the dot-notation path to the leaf value. For example { a: { b: { c: 1 } } } becomes { 'a.b.c': 1 }. Handle arrays as leaf values, not as objects to recurse into.

Sample answer outline

Recursive solution walks each key, appending to a prefix string. Base cases are when the value is a primitive or an array. Strong answers handle the empty object case and correctly type the return as Record<string, unknown>. A very strong answer also handles circular references with a Set of visited objects.

Reference implementation (typescript)

TypeScript
function flattenObject(
  obj: Record<string, unknown>,
  prefix = ''
): Record<string, unknown> {
  // implement
}

// Expected:
// flattenObject({ a: { b: 1 }, c: [1, 2] })
// => { 'a.b': 1, 'c': [1, 2] }

Expect these follow-ups

  • How would you write the inverse function that reconstructs the nested object from dot-notation keys?
  • Your function receives a value where a key contains a literal dot. How do you handle that?
recursionobjectstypescriptalgorithmsdata-transformation

As asked

Implement a sliding window rate limiter class in Node.js/TypeScript. Given a limit of N requests per windowMs milliseconds, the isAllowed(userId) method returns true if the request is allowed and false otherwise. Use only in-process data structures (no Redis).

Sample answer outline

Use a Map of userId to an array of timestamps. On each call, filter out timestamps older than windowMs, then check if the remaining count is below the limit before appending the current timestamp. Time complexity is O(N) per call in the worst case. Candidate should note this does not work across multiple processes and explain why.

Reference implementation (typescript)

TypeScript
class SlidingWindowRateLimiter {
  private limit: number;
  private windowMs: number;
  private store: Map<string, number[]>;

  constructor(limit: number, windowMs: number) {
    this.limit = limit;
    this.windowMs = windowMs;
    this.store = new Map();
  }

  isAllowed(userId: string): boolean {
    // implement
  }
}

Expect these follow-ups

  • What is the memory growth risk of storing timestamps per user and how do you bound it?
  • How would you change this to a fixed window algorithm and what edge case does that introduce?
algorithmsrate-limitingnodejstypescriptdata-structures

As asked

Write a function runWithConcurrency(tasks, concurrency) that accepts an array of zero-argument async functions and a maximum concurrency number, then executes them so that at most concurrency tasks run simultaneously. Return an array of results in the original order.

Sample answer outline

Use a pool pattern: maintain an active set, start up to concurrency tasks at once, and when one resolves start the next. Promise.all on a sliding window works but is wasteful. The clean implementation uses a queue and resolves results into a pre-allocated array indexed by position to preserve order. Candidate should handle rejected tasks and the empty array edge case.

Reference implementation (typescript)

TypeScript
async function runWithConcurrency<T>(
  tasks: Array<() => Promise<T>>,
  concurrency: number
): Promise<T[]> {
  // implement
  // tasks[i] should return results[i]
}

Expect these follow-ups

  • How would you add a timeout per task so a slow task does not block the whole pool?
  • Promise.allSettled exists. Why did we not just use it here?
asyncpromisesconcurrencytypescriptnodejs

As asked

You have a Postgres table of events with columns id (UUID), created_at (timestamptz), and user_id. Write a TypeScript function that accepts a cursor (the last seen row's created_at and id), a limit, and a user_id, and returns the next page of results using keyset pagination rather than OFFSET.

Sample answer outline

The WHERE clause should be (created_at, id) < (cursor.created_at, cursor.id) using row value comparison, ordered by (created_at DESC, id DESC), with a LIMIT of limit+1 to detect if there is a next page. Candidate should encode the cursor as a base64 string to avoid leaking internals, return hasNextPage from the extra row, and explain why OFFSET pagination breaks at scale.

Reference implementation (typescript)

TypeScript
interface Cursor { createdAt: Date; id: string; }
interface PageResult<T> { items: T[]; nextCursor: string | null; }

async function getEventsPage(
  db: Pool,
  userId: string,
  limit: number,
  cursor?: Cursor
): Promise<PageResult<Event>> {
  // implement with keyset pagination
}

Expect these follow-ups

  • Why does OFFSET-based pagination give wrong results when rows are inserted between pages?
  • How do you handle the first page request when there is no cursor yet?
paginationpostgrestypescriptdatabasesapi-design

As asked

Write a retry<T>(fn, options) utility in TypeScript that calls an async function fn up to maxAttempts times with exponential backoff. Add jitter (randomize the delay) to avoid thundering herd. Accept a shouldRetry predicate to distinguish transient from permanent errors.

Sample answer outline

Delay formula: Math.min(baseDelay * 2^attempt + Math.random() * jitterMs, maxDelay). shouldRetry predicate receives the caught error and returns boolean. If shouldRetry is false, re-throw immediately without consuming remaining attempts. Candidate should use a loop rather than recursion to avoid stack overflow on many attempts, and should not swallow the final error.

Reference implementation (typescript)

TypeScript
interface RetryOptions {
  maxAttempts: number;
  baseDelay: number;
  maxDelay: number;
  jitterMs?: number;
  shouldRetry?: (err: unknown) => boolean;
}

async function retry<T>(
  fn: () => Promise<T>,
  options: RetryOptions
): Promise<T> {
  // implement
}

Expect these follow-ups

  • How would you add a total timeout (not per-attempt) so the whole retry chain gives up after 30 seconds?
  • What is the thundering herd problem and why does jitter solve it?
asyncerror-handlingtypescriptresiliencealgorithms

As asked

Write a deepMerge(target, source) function in TypeScript that recursively merges source into target. Arrays should be concatenated, not overwritten. Primitive values in source overwrite target. Detect and throw on circular references.

Sample answer outline

Recursion with a WeakSet of visited objects on the source side detects circular references. Arrays are spread-concatenated. For objects, recurse into each key. Candidate should handle the case where source has a key whose value is an object but target has a primitive for the same key (source wins). TypeScript generic signature should return the merged type.

Reference implementation (typescript)

TypeScript
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
  // detect circular references
  // merge arrays by concatenation
  // merge objects recursively
  // primitives: source overwrites target
}

Expect these follow-ups

  • Object.assign does a shallow merge. When is that actually good enough and you should not reach for deepMerge?
  • How does structuredClone compare to your deepMerge for cloning use cases?
recursionobjectstypescriptalgorithmsedge-cases

As asked

Build a TypeScript EventEmitter class where the event names and their payload types are defined by a generic map. The class should support on(event, listener), off(event, listener), and emit(event, payload) methods, all fully type-safe without any use of any.

Sample answer outline

Use a generic type parameter EventMap extends Record<string, unknown>. The listeners store is a Map keyed by keyof EventMap. The on method infers the payload type from the event key via EventMap[K]. Emit calls each listener with the correctly typed payload. Strong answers also implement a once method using a wrapper listener that calls off on first invocation.

Reference implementation (typescript)

TypeScript
type EventMap = Record<string, unknown>;

class TypedEventEmitter<T extends EventMap> {
  private listeners: Map<keyof T, Array<(payload: T[keyof T]) => void>>;

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): this {
    // implement
  }
  off<K extends keyof T>(event: K, listener: (payload: T[K]) => void): this {
    // implement
  }
  emit<K extends keyof T>(event: K, payload: T[K]): void {
    // implement
  }
}

Expect these follow-ups

  • How would you add a wildcard listener that receives all events regardless of type?
  • What memory leak risk does this EventEmitter have and how does Node.js's built-in EventEmitter address it?
typescriptgenericsdesign-patternsevent-driventype-safety

As asked

You are building a workflow engine where each step has a list of steps it depends on. Write a function in TypeScript that accepts a map of step IDs to their dependency arrays and returns the steps in a valid execution order. If there is a cycle, throw an error.

Sample answer outline

Kahn's algorithm or DFS with a visited/in-progress color set both work. DFS with three states (unvisited, visiting, visited) detects cycles via the visiting state. Candidate should iterate all nodes to handle disconnected graphs, and return the nodes in reverse finish order for DFS or dequeue order for Kahn's. Strong answers note Kahn's is easier to reason about for cycle detection.

Reference implementation (typescript)

TypeScript
function topologicalSort(
  graph: Record<string, string[]>
): string[] {
  // graph[id] = list of dependency ids
  // return steps in valid execution order
  // throw if cycle detected
}

// Example:
// topologicalSort({ a: [], b: ['a'], c: ['a', 'b'] })
// => ['a', 'b', 'c']

Expect these follow-ups

  • Your graph has 10,000 nodes. Which algorithm is more memory-efficient and why?
  • How would you parallelize execution given the topological order?
graphsalgorithmstopological-sorttypescriptdata-structures

As asked

Without using react-window or react-virtualized, write a React component in TypeScript that renders a scrollable list of 100,000 items where only the visible rows are in the DOM. Assume each row has a fixed height of 40px.

Sample answer outline

Calculate the total container height as count * rowHeight. Track scrollTop from a scroll event listener. Derive startIndex = Math.floor(scrollTop / rowHeight) and endIndex = startIndex + visibleCount + overscan. Render only those rows positioned absolutely within the container using top = index * rowHeight. A strong answer memoizes the row slice and uses useCallback for the scroll handler to avoid re-renders.

Reference implementation (typescript)

TypeScript
interface VirtualListProps<T> {
  items: T[];
  rowHeight: number;
  renderRow: (item: T, index: number) => React.ReactNode;
  height: number; // visible container height
}

function VirtualList<T>({ items, rowHeight, renderRow, height }: VirtualListProps<T>) {
  // implement
}

Expect these follow-ups

  • Your rows have variable heights. How does the implementation change?
  • How would you add keyboard navigation so the user can arrow-key through all 100,000 rows?
reactperformancedomfrontendvirtualisation