PracHub
QuestionsCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates skills in concurrent programming and stateful selection logic (round-robin load balancing), data structure invariants and debugging (custom hash map hashing vs equality, collision resolution, resizing and iterator correctness), simple in-memory caching strategies with TTL or size-based eviction, and unit test design.

  • medium
  • DoorDash
  • Coding & Algorithms
  • Software Engineer

Debug round-robin, DashMap, and simple cache

Company: DoorDash

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

You are given a service that routes requests to a list of nodes, each marked as either available or unavailable. The pickNode() function is intended to perform round-robin selection while skipping unavailable nodes, but a failing unit test shows it occasionally returns an unavailable node. Debug and fix the implementation: ( 1) maintain a global (thread-safe) index so selection does not reset per call; ( 2) ensure status checks are robust (e.g., use an enum or constant, not fragile string literals); ( 3) define behavior when all nodes are unavailable; and ( 4) update/add a unit test where one node is unavailable and one is available so the selector repeatedly returns the available node. As a separate debugging task, you are given a buggy custom hash map named DashMap. Diagnose and fix issues around key hashing vs equality, collision handling, resizing/rehashing, and iterator behavior. Finally, implement a very simple in-memory cache backed by a map with get/set and optional TTL or size-based eviction, and write basic tests for it.

Quick Answer: This question evaluates skills in concurrent programming and stateful selection logic (round-robin load balancing), data structure invariants and debugging (custom hash map hashing vs equality, collision resolution, resizing and iterator correctness), simple in-memory caching strategies with TTL or size-based eviction, and unit test design.

Part 1: Round-Robin Available Node Selector

Implement a **round-robin selector** over a fixed list of service nodes, where each node can be toggled available or unavailable while you keep handing out the next available node in circular order. ## What to implement ```python def solution(nodes, operations): ... ``` - **`nodes`** — a list of `(node_id, status)` tuples describing the nodes, in their fixed order. `node_id` is a unique string; `status` is `1` (**available**) or `0` (**unavailable**). - **`operations`** — a list of operation tuples to process **in the given order**. Return a **list** containing one result for **each `pick` operation only**, in the order those picks were processed. ## Operations Process operations one at a time. There are two kinds: - **`('pick',)`** — Select and return the **next available node**, then record its result in the output list. Selection uses a **persistent round-robin pointer** (described below). - **`('set', node_id, status)`** — Update that node's availability to `status` (`1` = available, `0` = unavailable). This operation produces **no output**. Any empty/falsy operation in the list is ignored. ## Round-robin pointer rules - The pointer is an **index into the node list** and starts at index `0`. - A `pick` returns the node at the **smallest index `>=` the current pointer that is currently available**. If no available node exists at or after the pointer, the search **wraps around** and continues from index `0`, returning the first available node before the pointer. - After a successful pick of the node at index `idx`, advance the pointer to `(idx + 1) % len(nodes)`, so the next `pick` continues from just after the node just chosen. - The pointer **persists across picks** (and is unaffected by `set` operations except through availability changes). ## Return values for a pick - On success, append the chosen node's **`node_id`** (a string). - If **no node is currently available** (every node has status `0`, or there are no nodes at all), append **`-1`** (the integer). In this case the pointer is left unchanged. ## Notes & edge cases - Status is **numeric** (`0`/`1`); compare against these numeric constants rather than string-based checks. - If `nodes` is empty, every `pick` returns `-1`. - A `set` may change a node's status to a value it already has; this is a valid no-op update. ## Examples - `nodes = [('A', 0), ('B', 1)]`, `operations = [('pick',), ('pick',), ('pick',)]` → `['B', 'B', 'B']` (A is unavailable, so every pick returns B). - `nodes = [('A', 1), ('B', 1), ('C', 1)]`, `operations = [('pick',), ('pick',), ('set', 'B', 0), ('pick',), ('pick',), ('set', 'A', 0), ('set', 'C', 0), ('pick',)]` → `['A', 'B', 'C', 'A', -1]`. - `nodes = []`, `operations = [('pick',), ('pick',)]` → `[-1, -1]`. - `nodes = [('A', 1), ('B', 0), ('C', 1)]`, `operations = [('pick',), ('pick',), ('pick',)]` → `['A', 'C', 'A']` (B is skipped; after C the pointer wraps back to A). ## Constraints - `0 <= len(nodes) <= 200000` - `0 <= len(operations) <= 200000` - Each `node_id` is unique. - Each `status` is either `0` or `1`.

Constraints

  • 0 <= len(nodes) <= 200000
  • 0 <= len(operations) <= 200000
  • Each node_id is unique
  • Each status is either 0 or 1

Examples

Input: ([('A', 0), ('B', 1)], [('pick',), ('pick',), ('pick',)])

Expected Output: ['B', 'B', 'B']

Explanation: Only B is available, so every pick returns B.

Input: ([('A', 1), ('B', 1), ('C', 1)], [('pick',), ('pick',), ('set', 'B', 0), ('pick',), ('pick',), ('set', 'A', 0), ('set', 'C', 0), ('pick',)])

Expected Output: ['A', 'B', 'C', 'A', -1]

Explanation: The pointer keeps moving forward across picks. After all nodes become unavailable, pick returns -1.

Input: ([], [('pick',), ('pick',)])

Expected Output: [-1, -1]

Explanation: With no nodes at all, every pick fails.

Input: ([('A', 1), ('B', 0), ('C', 1)], [('pick',), ('pick',), ('pick',)])

Expected Output: ['A', 'C', 'A']

Explanation: The selector wraps around and keeps skipping the unavailable node B.

Hints

  1. Keep one pointer that survives across all pick operations instead of restarting at index 0 each time.
  2. A segment tree or other indexed structure can help find the next available node quickly after updates.

Part 2: Implement and Repair DashMap

Implement a custom hash map called **DashMap** from scratch using **separate chaining**, then replay a sequence of operations against it and return the results. Write a function with this signature: ```python def solution(initial_capacity, operations): ... ``` ## What to build DashMap stores **string keys** mapped to **integer values**. Build it on top of an array of buckets, where each bucket is a list that holds every `(key, value)` pair whose key hashes to that slot. **Hash function (use exactly this).** For the current table `capacity`, the slot for a key is: ``` hash(key) = sum(ord(c) for c in key) % capacity ``` This deterministic hash makes collisions easy to produce. Because distinct keys can share the same slot, you must compare keys with **normal string equality** (`k == key`) when searching a bucket, so two different keys with the same hash still coexist correctly. **Capacity floor.** Treat the working capacity as at least `2` (i.e. `max(2, initial_capacity)`), so the table is never empty and you never take a modulus by `0`. ## Inputs - `initial_capacity` — a non-negative integer, the requested starting number of buckets (floored to a minimum of `2` as described above). - `operations` — a list of operation tuples to apply **in order**. Each operation is one of: - `('put', key, value)` — insert or update `key` with `value`. - `('get', key)` — look up `key`. - `('remove', key)` — delete `key`. - `('items',)` — snapshot all current entries. ## Operation semantics - **`('put', key, value)`** — If `key` already exists, overwrite its value in place (the size does not change). Otherwise, add a new `(key, value)` entry and increase the size. This operation produces **no value** in the result list. - **`('get', key)`** — Return the value mapped to `key`, or `None` if `key` is not present. - **`('remove', key)`** — If `key` exists, delete it and return `True`. If `key` is not present, return `False`. - **`('items',)`** — Return a list of all current live `(key, value)` pairs, each appearing **exactly once**, **sorted by key** in ascending order for deterministic output. ## Resizing After a **new** key is inserted by a `put` (not on an update of an existing key), check the load factor. Whenever ``` size > capacity * 0.75 ``` (strictly greater than `0.75`), **double the capacity** and **rehash** every existing entry into the larger table using the new capacity. Resizing only ever happens on growth from a `put`; `get`, `remove`, and `items` never trigger it. ## Return value Return a single list containing the result of each operation that produces an output, in the order those operations were applied. Concretely, append one entry per `get` (its value or `None`), one entry per `remove` (`True`/`False`), and one entry per `items` (the sorted list of pairs). `put` operations contribute nothing to this list. ## Example For `initial_capacity = 2` and operations: ``` [('put', 'ab', 1), ('put', 'ba', 2), ('get', 'ab'), ('get', 'ba'), ('items',)] ``` `'ab'` and `'ba'` hash to the same slot but must coexist. The function returns: ``` [1, 2, [('ab', 1), ('ba', 2)]] ``` ## Constraints - `0 <= initial_capacity <= 10000` - `0 <= len(operations) <= 20000` - Keys are strings of length `0` to `100`. - Values are integers.

Constraints

  • 0 <= initial_capacity <= 10000
  • 0 <= len(operations) <= 20000
  • Keys are strings of length 0 to 100
  • Values are integers

Examples

Input: (2, [('put', 'ab', 1), ('put', 'ba', 2), ('get', 'ab'), ('get', 'ba'), ('items',)])

Expected Output: [1, 2, [('ab', 1), ('ba', 2)]]

Explanation: 'ab' and 'ba' have the same hash under the given function, so this checks collision handling.

Input: (4, [('put', 'aa', 1), ('put', 'aa', 5), ('get', 'aa'), ('items',)])

Expected Output: [5, [('aa', 5)]]

Explanation: Inserting the same key again should update the existing entry, not create a duplicate.

Input: (2, [('put', 'a', 1), ('put', 'b', 2), ('put', 'c', 3), ('put', 'd', 4), ('get', 'c'), ('remove', 'b'), ('get', 'b'), ('items',)])

Expected Output: [3, True, None, [('a', 1), ('c', 3), ('d', 4)]]

Explanation: This forces resizing and then verifies that all remaining entries are still retrievable and iterable.

Input: (4, [('get', 'x'), ('remove', 'x'), ('items',)])

Expected Output: [None, False, []]

Explanation: Edge case with an empty map.

Hints

  1. A matching hash is not enough; inside a bucket you still need to compare actual keys for equality.
  2. When capacity changes, bucket indices change too, so every existing entry must be inserted again using the new modulus.

Part 3: Tiny In-Memory Cache with TTL and LRU Eviction

Implement a fixed-capacity **in-memory cache** that supports **per-entry TTL (time-to-live) expiration** and **LRU (least-recently-used) eviction**. Implement the function: ```python def solution(capacity, operations): ``` - `capacity` is a non-negative integer: the maximum number of live entries the cache may hold. - `operations` is a list of operation tuples to process **in order**. Each tuple's first element is its kind (`'set'`, `'get'`, or `'keys'`), and every operation carries an integer timestamp. **Timestamps are non-decreasing** across the operation list. Return a **list** containing one result for each `'get'` and `'keys'` operation, in the order those operations occur. `'set'` operations produce **no** output. ## Recency model The cache tracks how recently each live key was used, from **least recently used (LRU)** to **most recently used (MRU)**: - A `'set'` makes its key the **most recently used** entry. - A **successful** `'get'` (the key is still live) also makes that key the **most recently used** entry. ## Expiration (applied before every operation) Before processing **any** operation at timestamp `now`, first **remove all expired entries**. - An entry created by `('set', key, value, ttl, time)` with `ttl is not None` expires at `time + ttl`. It is considered **expired (unavailable) at that exact instant** — i.e. an entry is removed once `now >= expiry_time`. - An entry whose `ttl` is `None` **never expires**. ## Operations **`('set', key, value, ttl, time)`** — Store (or overwrite) `key`. - `ttl` is either `None` (no expiration) or a **positive integer** (`>= 1`); if not `None`, the entry expires at `time + ttl`. - Storing or overwriting a key marks it as **most recently used**. - After the insert, if the number of live entries **exceeds** `capacity`, **evict the least recently used live entry**. - **Special case:** if `capacity <= 0`, the cache holds nothing — the key is **not** stored, and if it already existed it is removed. - A `'set'` appends nothing to the output list. **`('get', key, time)`** — Look up `key`. - If the key is still live, append its **value** to the output and mark the key as **most recently used**. - Otherwise, append `None`. **`('keys', time)`** — Append a **list of the currently live keys**, ordered from **least recently used to most recently used**. ## Constraints - `0 <= capacity <= 100000` - `0 <= len(operations) <= 100000` - Operation timestamps are non-decreasing. - `ttl` is either `None` or an integer `>= 1`. ## Examples Given `capacity = 2` and operations: ``` ('set', 'a', 1, None, 0) ('set', 'b', 2, None, 1) ('get', 'a', 2) -> 1 (a becomes most recently used; order is now b, a) ('set', 'c', 3, None, 3) -> evicts b (the LRU live entry) ('get', 'b', 4) -> None (b was evicted) ('keys', 4) -> ['a', 'c'] ``` Returns `[1, None, ['a', 'c']]`. Given `capacity = 2`: ``` ('set', 'x', 10, 2, 0) -> x expires at time 2 ('get', 'x', 1) -> 10 ('get', 'x', 2) -> None (expired exactly at time 2) ('keys', 2) -> [] ``` Returns `[10, None, []]`.

Constraints

  • 0 <= capacity <= 100000
  • 0 <= len(operations) <= 100000
  • Operation timestamps are non-decreasing
  • ttl is either None or an integer >= 1

Examples

Input: (2, [('set', 'a', 1, None, 0), ('set', 'b', 2, None, 1), ('get', 'a', 2), ('set', 'c', 3, None, 3), ('get', 'b', 4), ('keys', 4)])

Expected Output: [1, None, ['a', 'c']]

Explanation: After 'get a', A becomes most recently used, so inserting C evicts B.

Input: (2, [('set', 'x', 10, 2, 0), ('get', 'x', 1), ('get', 'x', 2), ('keys', 2)])

Expected Output: [10, None, []]

Explanation: The entry for X expires exactly at time 2.

Input: (2, [('set', 'a', 1, 5, 0), ('set', 'b', 2, None, 1), ('set', 'a', 7, None, 2), ('get', 'a', 6), ('keys', 6)])

Expected Output: [7, ['b', 'a']]

Explanation: Overwriting A removes the old TTL logically and keeps A as the most recently used key.

Input: (0, [('set', 'a', 1, None, 0), ('get', 'a', 1), ('keys', 1)])

Expected Output: [None, []]

Explanation: With zero capacity, nothing can be stored.

Hints

  1. Use a hash map for fast lookup and another structure to track recency order for LRU eviction.
  2. For TTL, a min-heap of expiration times works well if you attach a version number so stale heap entries can be ignored.
Last updated: Apr 27, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • AI Coding Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Validate a Shopping Cart - DoorDash (medium)
  • Implement Timeout Refund Workflow - DoorDash (medium)
  • Calculate Driver Payments - DoorDash (medium)
  • Maximize Chef Assignment Profit - DoorDash (medium)
  • Compute Courier Delivery Pay - DoorDash (easy)