PracHub
QuestionsCoachesLearningGuidesInterview Prep

Consistent Hashing Ring with Virtual Nodes for Shard Rebalancing

Company: OpenAI

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: hard

Interview Round: Technical Screen

# Consistent Hashing Ring with Virtual Nodes for Shard Rebalancing A distributed key-value store spreads its keys across a set of **shards**. When a shard is added or removed, you want to remap as few keys as possible — naive modulo hashing (`hash(key) % N`) reshuffles almost everything whenever `N` changes. Implement a **consistent hashing ring with virtual nodes**, which bounds the fraction of keys that move when the shard set changes and keeps the load spread evenly. Implement a class `ShardRing` that maintains a hash ring on the integer interval `[0, 2^32)` (positions wrap around: the successor of `2^32 - 1` is `0`). ## Hashing All positions on the ring are produced by the **32-bit FNV-1a** hash of a UTF-8 string. Compute `fnv1a_32(s)` as: ``` hash = 2166136261 # 0x811c9dc5 for each byte b in the UTF-8 encoding of s: hash = hash XOR b hash = (hash * 16777619) mod 2^32 # 16777619 = 0x01000193 return hash # a 32-bit unsigned integer ``` Each shard with id `shard_id` places `vnodes` **virtual nodes** on the ring. The position of virtual node `i` (for `i` in `0 .. vnodes-1`) is: ``` fnv1a_32(shard_id + "#" + str(i)) ``` ## Methods to implement - `__init__(self, vnodes: int)` — create an empty ring. Every shard added later contributes exactly `vnodes` virtual nodes. `1 <= vnodes <= 1000`. - `add_shard(self, shard_id: str) -> None` — add a shard and its `vnodes` virtual nodes. The same `shard_id` is never added twice without first being removed. - `remove_shard(self, shard_id: str) -> None` — remove a shard and all of its virtual nodes. The `shard_id` is guaranteed to be currently present. - `get_shard(self, key: str) -> str` — return the id of the shard that owns `key`. Ownership is the **first virtual node found clockwise** (in increasing position order) starting at `fnv1a_32(key)`; if `fnv1a_32(key)` is greater than every virtual-node position, wrap around to the smallest position on the ring. If the ring currently has no shards, return the empty string `""`. ## Tie-breaking If two or more virtual nodes (from different shards, or different replicas) hash to the **same** ring position, order them at that position by `shard_id` ascending, then by replica index `i` ascending. `get_shard` returns the first such virtual node encountered clockwise under this ordering. ## Requirements - `get_shard` must run in `O(log V)` time, where `V` is the current number of virtual nodes on the ring (binary search over sorted positions). `add_shard` / `remove_shard` may be `O(vnodes * log V)`. - The minimal-movement property must hold structurally: because keys are routed to the nearest clockwise virtual node, removing a shard reassigns only that shard's keys (to their next clockwise shards), and adding a shard only pulls keys off the segments its virtual nodes land on — no other key changes owner. ## Example Suppose `vnodes = 100` and you `add_shard("A")`, then `add_shard("B")`. A key routes to whichever of A's or B's 100 virtual nodes is nearest clockwise from the key's hash, so the keys split between A and B roughly in proportion to how the 200 virtual nodes interleave around the ring (close to 50/50 for well-spread hashes). Now `add_shard("C")`: only the keys whose hashes fall just before one of C's 100 new virtual-node positions move to C; every other key keeps its previous owner. If you then `remove_shard("B")`, only keys that were owned by B's virtual nodes move — each to the next virtual node clockwise (an A or C node) — while keys already on A or C are untouched. ## Constraints - Up to `10^5` total operations across `add_shard`, `remove_shard`, and `get_shard`. - `vnodes` is fixed for the lifetime of a `ShardRing` instance. - `shard_id` and `key` are non-empty ASCII strings up to 64 characters. - All arithmetic on hashes is over unsigned 32-bit integers (`mod 2^32`).
Implement a consistent hashing ring with virtual nodes over the integer interval [0, 2^32) (positions wrap: the successor of 2^32-1 is 0). Because the console runs one function, drive the ring through a command list: you are given `operations` (method names) and `args` (the arguments for each), and you return one result per operation.\n\nSupport these methods:\n- "ShardRing" with args [vnodes]: create an empty ring where every shard contributes exactly `vnodes` virtual nodes (1 <= vnodes <= 1000). Return None.\n- "add_shard" with args [shard_id]: add a shard; its virtual node i sits at fnv1a_32(shard_id + "#" + str(i)) for i in 0..vnodes-1. Return None.\n- "remove_shard" with args [shard_id]: remove the shard and all of its virtual nodes. Return None.\n- "get_shard" with args [key]: return the owning shard id, i.e. the first virtual node clockwise (increasing position) starting at fnv1a_32(key); if the key hash exceeds every position, wrap to the smallest position. Return "" if the ring has no shards.\n\nHashing (32-bit FNV-1a of the UTF-8 bytes): hash = 2166136261; for each byte b: hash ^= b; hash = (hash * 16777619) mod 2^32.\n\nTie-breaking: if virtual nodes share a position, order them by shard_id ascending, then replica index i ascending; get_shard returns the first such node clockwise. get_shard must be O(log V) (binary search over sorted positions).

Constraints

  • 1 <= vnodes <= 1000 (fixed for the ring's lifetime)
  • Up to 10^5 total operations across add_shard, remove_shard, get_shard
  • shard_id and key are non-empty ASCII strings up to 64 characters
  • The same shard_id is never added twice without being removed first; remove targets a present shard
  • All hash arithmetic is unsigned 32-bit (mod 2^32); ring positions lie in [0, 2^32)

Examples

Input: (["ShardRing", "get_shard"], [[3], ["anykey"]])

Expected Output: [None, ""]

Explanation: Empty ring: get_shard has no virtual nodes to route to, so it returns the empty string.

Input: (["ShardRing", "add_shard", "get_shard", "get_shard", "get_shard"], [[5], ["A"], ["k1"], ["k2"], ["user:42"]])

Expected Output: [None, None, "A", "A", "A"]

Explanation: With only shard A on the ring, every key routes clockwise to one of A's 5 virtual nodes, so all keys are owned by A.

Input: (["ShardRing", "add_shard", "add_shard", "get_shard", "get_shard", "get_shard", "get_shard", "get_shard", "get_shard"], [[50], ["A"], ["B"], ["k1"], ["k2"], ["k3"], ["user:42"], ["user:7"], ["alpha"]])

Expected Output: [None, None, None, "B", "B", "B", "A", "B", "B"]

Explanation: A and B each place 50 virtual nodes; keys split by whichever virtual node is nearest clockwise. Here k1/k2/k3, user:7 and alpha land on B's arcs while user:42 lands on A's.

Input: (["ShardRing", "add_shard", "add_shard", "add_shard", "get_shard", "get_shard", "get_shard", "remove_shard", "get_shard", "get_shard", "get_shard"], [[100], ["A"], ["B"], ["C"], ["k1"], ["k2"], ["k3"], ["B"], ["k1"], ["k2"], ["k3"]])

Expected Output: [None, None, None, None, "B", "B", "B", None, "A", "A", "A"]

Explanation: With A, B, C present, k1/k2/k3 are owned by B. Removing B reassigns exactly those keys to the next virtual node clockwise (A here); keys already on A or C would be untouched -- the minimal-movement property.

Input: (["ShardRing", "add_shard", "add_shard", "get_shard", "get_shard", "get_shard", "get_shard"], [[1], ["alpha"], ["beta"], ["k1"], ["k2"], ["k3"], ["key-1"]])

Expected Output: [None, None, None, "alpha", "alpha", "alpha", "beta"]

Explanation: vnodes=1 leaves two positions: beta#0=1965256788 and alpha#0=3950171640. k1/k2/k3 (hashes ~2.5B) fall between them so route clockwise to alpha; key-1 (hash 1474311238) is below beta's position so it routes to beta.

Hints

  1. Store every virtual node as a (position, shard_id, replica_index) tuple in one list kept sorted; this list IS the ring.
  2. For get_shard, binary-search for the first entry whose position >= fnv1a_32(key); searching with the 1-tuple (h,) makes same-position ties fall to the smallest (shard_id, replica) automatically.
  3. If the binary search runs off the end, the key wrapped around: return the shard at index 0 (the smallest position).
  4. Get the mod-2^32 FNV-1a right by masking with 0xFFFFFFFF (or letting fixed-width unsigned ints wrap); an off-by-one in the multiply constant (16777619) or seed (2166136261) changes every position.
Last updated: Jul 1, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,000+ 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

  • Infection Spread Simulation with Death Threshold - OpenAI (medium)
  • Spreading Contagion on a Grid - OpenAI (medium)
  • Streaming Entropy with Numerical Stability - OpenAI (hard)
  • Implement a Distributed Rate Limiter - OpenAI (medium)
  • Compute Plant Infection Time - OpenAI (medium)