Debug a round-robin load balancer, then extend it to consistent hashing

Read the full interview experience this question came from →

Quick Overview

Find and fix the bugs in a round-robin load balancer using its failing tests, without rewriting the tests, then extend it to consistent hashing so each key maps to a stable server. It tests systematic debugging, index bookkeeping on add and remove, hash rings with virtual nodes and minimal key movement.

Debug a round-robin load balancer, then extend it to consistent hashing

Company: DoorDash

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

In a debugging round you receive a small round-robin load balancer, which hands out the servers of a pool in turn, together with test cases that it currently fails. Find and fix the bugs. The final step of the round is to change how the balancer assigns requests so that it uses consistent hashing. The report does not include the original code. The version below is a practice stand-in of the same shape: a round-robin balancer with bugs and a test file. It must follow these rules: - The balancer keeps its own copy of the server list it is given; later changes to the caller's list do not affect it. Server names in the list are distinct. - `next_server()` returns the servers in list order, starting with the first, and wraps around after the last. It returns `None` when the pool is empty. - `add_server(s)` puts `s` at the end of the rotation. Adding a server that is already in the pool does nothing. - `remove_server(s)` takes `s` out of the pool. Removing a server that is not in the pool does nothing. After a removal, the rotation continues with the server that would have come next among the remaining servers. - Keep the given tests and their inputs as they are: fix the code, not the tests. ```python # balancer.py class RoundRobinBalancer: def __init__(self, servers): self.servers = servers self.index = 0 def next_server(self): server = self.servers[self.index] self.index = self.index + 1 % len(self.servers) return server def add_server(self, server): self.servers.append(server) def remove_server(self, server): position = self.servers.index(server) self.servers.remove(server) if position <= self.index: self.index -= 1 ``` ```python # test_balancer.py from balancer import RoundRobinBalancer def test_rotation_wraps(): lb = RoundRobinBalancer(["a", "b", "c"]) assert [lb.next_server() for _ in range(5)] == ["a", "b", "c", "a", "b"] def test_remove_next_server(): lb = RoundRobinBalancer(["a", "b", "c"]) assert lb.next_server() == "a" lb.remove_server("b") assert [lb.next_server() for _ in range(3)] == ["c", "a", "c"] def test_remove_unknown_and_add_duplicate_are_noops(): lb = RoundRobinBalancer(["a", "b"]) lb.remove_server("z") lb.add_server("a") assert [lb.next_server() for _ in range(4)] == ["a", "b", "a", "b"] def test_caller_list_is_copied(): pool = ["a", "b"] lb = RoundRobinBalancer(pool) pool.append("c") assert [lb.next_server() for _ in range(3)] == ["a", "b", "a"] def test_empty_pool(): lb = RoundRobinBalancer(["a"]) lb.remove_server("a") assert lb.next_server() is None ``` ### Clarifying Questions - Must the public method names and return values stay exactly as the tests use them? - Is the balancer called from several threads at once? - Should an unhealthy server be skipped, or is health checking out of scope? ### Part 1 — Fix the round-robin balancer Make every given test pass by fixing the balancer, explain each bug you find and why it caused a failure, and add any test you think is missing. ```hint Reproduce before you restyle Run the tests unchanged and work from the first failure's error message and line before touching anything that is only a matter of style. ``` ```hint Follow the pointer For each operation, write down where the stored index should point afterwards, especially when the removed server is the next one or the last one in the list. ``` #### What This Part Should Cover - A systematic loop: reproduce, isolate, explain the root cause, make the smallest fix, rerun everything - Every defect, including those that only appear after removals or on an empty pool - Index bookkeeping that keeps the rotation fair after additions and removals - The given tests left unchanged, plus regression tests for cases they miss ### Part 2 — Switch to consistent hashing Requests now carry a key, such as a user id, and each key must go to the same server as long as the pool does not change. Add `get_server(key)` based on consistent hashing, keeping `add_server` and `remove_server`, so that adding or removing one server moves only the keys that have to move. ```hint One circle for keys and servers Picture servers and keys placed on the same circle of hash values, and ask which keys change owner when one server joins. ``` #### Clarifying Questions for this Part - How many servers and keys must it handle, and how even must the load be? - Do servers have different capacities that the assignment should respect? - Must the mapping be identical across processes and restarts? #### What This Part Should Cover - A ring of sorted positions with a fast lookup of the next position - Virtual nodes for an even spread, and a hash function that is stable across processes - Behavior on add, remove and an empty pool, with the complexity of each operation - Tests that show stability and minimal key movement ### What a Strong Answer Covers - A structured search for bugs driven by the failing tests, not rewrites for style - A correct root-cause explanation for each fix - The original tests and inputs untouched, with focused new tests added - A correct consistent hashing implementation with virtual nodes and a stable hash - The complexity and trade-offs of both assignment schemes, with time managed so the second step is reached ### Follow-up Questions - Make the balancer safe when many threads call it at once. What exactly must be atomic? - Add server weights so that a larger server gets proportionally more traffic, in both the round-robin and the consistent hashing versions. - How many virtual nodes per server would you choose, and how would you measure whether the load is even? - A server drops out briefly and comes back. How do you avoid moving its keys away and back?

Overview: Find and fix the bugs in a round-robin load balancer using its failing tests, without rewriting the tests, then extend it to consistent hashing so each key maps to a stable server. It tests systematic debugging, index bookkeeping on add and remove, hash rings with virtual nodes and minimal key movement.

Read the full DoorDash Software Engineer interview experience this question came from

DoorDash logo
DoorDash
Oct 28, 2026
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
0
0

In a debugging round you receive a small round-robin load balancer, which hands out the servers of a pool in turn, together with test cases that it currently fails. Find and fix the bugs. The final step of the round is to change how the balancer assigns requests so that it uses consistent hashing.

The report does not include the original code. The version below is a practice stand-in of the same shape: a round-robin balancer with bugs and a test file. It must follow these rules:

  • The balancer keeps its own copy of the server list it is given; later changes to the caller's list do not affect it. Server names in the list are distinct.
  • next_server() returns the servers in list order, starting with the first, and wraps around after the last. It returns None when the pool is empty.
  • add_server(s) puts s at the end of the rotation. Adding a server that is already in the pool does nothing.
  • remove_server(s) takes s out of the pool. Removing a server that is not in the pool does nothing. After a removal, the rotation continues with the server that would have come next among the remaining servers.
  • Keep the given tests and their inputs as they are: fix the code, not the tests.
# balancer.py
class RoundRobinBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.index = 0

    def next_server(self):
        server = self.servers[self.index]
        self.index = self.index + 1 % len(self.servers)
        return server

    def add_server(self, server):
        self.servers.append(server)

    def remove_server(self, server):
        position = self.servers.index(server)
        self.servers.remove(server)
        if position <= self.index:
            self.index -= 1
# test_balancer.py
from balancer import RoundRobinBalancer


def test_rotation_wraps():
    lb = RoundRobinBalancer(["a", "b", "c"])
    assert [lb.next_server() for _ in range(5)] == ["a", "b", "c", "a", "b"]


def test_remove_next_server():
    lb = RoundRobinBalancer(["a", "b", "c"])
    assert lb.next_server() == "a"
    lb.remove_server("b")
    assert [lb.next_server() for _ in range(3)] == ["c", "a", "c"]


def test_remove_unknown_and_add_duplicate_are_noops():
    lb = RoundRobinBalancer(["a", "b"])
    lb.remove_server("z")
    lb.add_server("a")
    assert [lb.next_server() for _ in range(4)] == ["a", "b", "a", "b"]


def test_caller_list_is_copied():
    pool = ["a", "b"]
    lb = RoundRobinBalancer(pool)
    pool.append("c")
    assert [lb.next_server() for _ in range(3)] == ["a", "b", "a"]


def test_empty_pool():
    lb = RoundRobinBalancer(["a"])
    lb.remove_server("a")
    assert lb.next_server() is None

Clarifying Questions Guidance

  • Must the public method names and return values stay exactly as the tests use them?
  • Is the balancer called from several threads at once?
  • Should an unhealthy server be skipped, or is health checking out of scope?

Part 1 — Fix the round-robin balancer

Make every given test pass by fixing the balancer, explain each bug you find and why it caused a failure, and add any test you think is missing.

What This Part Should Cover Guidance

  • A systematic loop: reproduce, isolate, explain the root cause, make the smallest fix, rerun everything
  • Every defect, including those that only appear after removals or on an empty pool
  • Index bookkeeping that keeps the rotation fair after additions and removals
  • The given tests left unchanged, plus regression tests for cases they miss

Part 2 — Switch to consistent hashing

Requests now carry a key, such as a user id, and each key must go to the same server as long as the pool does not change. Add get_server(key) based on consistent hashing, keeping add_server and remove_server, so that adding or removing one server moves only the keys that have to move.

Clarifying Questions for this Part Guidance

  • How many servers and keys must it handle, and how even must the load be?
  • Do servers have different capacities that the assignment should respect?
  • Must the mapping be identical across processes and restarts?

What This Part Should Cover Guidance

  • A ring of sorted positions with a fast lookup of the next position
  • Virtual nodes for an even spread, and a hash function that is stable across processes
  • Behavior on add, remove and an empty pool, with the complexity of each operation
  • Tests that show stability and minimal key movement

What a Strong Answer Covers Guidance

  • A structured search for bugs driven by the failing tests, not rewrites for style
  • A correct root-cause explanation for each fix
  • The original tests and inputs untouched, with focused new tests added
  • A correct consistent hashing implementation with virtual nodes and a stable hash
  • The complexity and trade-offs of both assignment schemes, with time managed so the second step is reached

Follow-up Questions Guidance

  • Make the balancer safe when many threads call it at once. What exactly must be atomic?
  • Add server weights so that a larger server gets proportionally more traffic, in both the round-robin and the consistent hashing versions.
  • How many virtual nodes per server would you choose, and how would you measure whether the load is even?
  • A server drops out briefly and comes back. How do you avoid moving its keys away and back?