Consistent Hashing Implementation
Working Source-code implementations of consistent hashing with virtual nodes, hash ring, and key distribution.
Quick answer: Consistent Hashing Implementation
Consistent Hashing Implementation is covered here as a practical system design resource. Working Source-code implementations of consistent hashing with virtual nodes, hash ring, and key distribution. Use it as a curated reference while connecting the original engineering lesson to practical system design decisions.
Focus on the problem being solved, the main design choice, the failure mode, and the tradeoff you would defend in front of a senior engineer. This page is part of the DosiBridge system design knowledge graph, so it links related concepts, interview problems, tradeoffs, and references instead of leaving the topic isolated.
Consistent Hashing — Code Lab
Consistent hashing is a technique that minimizes key redistribution when nodes are added or removed from a distributed system. This implementation demonstrates the core algorithm with virtual nodes for balanced distribution.
How the Algorithm Works
- Hash Ring: Both servers and keys are mapped onto a circular hash space (0 to 2{}32 - 1)
- Virtual Nodes: Each physical server is mapped to multiple positions on the ring for even distribution
- Key Assignment: A key is assigned to the first server found by walking clockwise from the key's hash position
- Node Changes: When a server is added/removed, only K/N keys need to be remapped (K = total keys, N = total nodes)
Complexity Analysis
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Node | O(K log N) | O(N * V) |
| Remove Node | O(K log N) | O(N * V) |
| Lookup Key | O(log N * V) | O(1) |
Where N = number of nodes, V = virtual nodes per server, K = total keys.
Java Implementation
package implementations.java.consistent_hashing;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class ConsistentHashing {
private final int numReplicas; // Number of virtual nodes per server
private final TreeMap<Long, String> ring; // Hash ring storing virtual nodes
private final Set<String> servers; // Set of physical servers
public ConsistentHashing(List<String> servers, int numReplicas) {
this.numReplicas = numReplicas;
this.ring = new TreeMap<>();
this.servers = new HashSet<>();
// Add each server to the hash ring
for (String server : servers) {
addServer(server);
}
}
private long hash(String key) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(key.getBytes());
byte[] digest = md.digest();
return ((long) (digest[0] & 0xFF) << 24) |
((long) (digest[1] & 0xFF) << 16) |
((long) (digest[2] & 0xFF) << 8) |
((long) (digest[3] & 0xFF));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found", e);
}
}
public void addServer(String server) {
servers.add(server);
for (int i = 0; i < numReplicas; i++) {
long hash = hash(server + "-" + i); // Unique hash for each virtual node
ring.put(hash, server);
}
}
public void removeServer(String server) {
if (servers.remove(server)) {
for (int i = 0; i < numReplicas; i++) {
long hash = hash(server + "-" + i);
ring.remove(hash);
}
}
}
public String getServer(String key) {
if (ring.isEmpty()) {
return null; // No servers available
}
long hash = hash(key);
// Find the closest server in a clockwise direction
Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
if (entry == null) {
// If we exceed the highest node, wrap around to the first node
entry = ring.firstEntry();
}
return entry.getValue();
}
public static void main(String[] args) {
List<String> servers = Arrays.asList("S0", "S1", "S2", "S3", "S4", "S5");
ConsistentHashing ch = new ConsistentHashing(servers, 3);
// Step 2: Assign requests (keys) to servers
System.out.println("UserA is assigned to: " + ch.getServer("UserA"));
System.out.println("UserB is assigned to: " + ch.getServer("UserB"));
// Step 3: Add a new server dynamically
ch.addServer("S6");
System.out.println("UserA is now assigned to: " + ch.getServer("UserA"));
// Step 4: Remove a server dynamically
ch.removeServer("S2");
System.out.println("UserB is now assigned to: " + ch.getServer("UserB"));
}
}
Python Implementation
import hashlib
import bisect
class ConsistentHashing:
def __init__(self, servers, num_replicas=3):
"""
Initializes the consistent hashing ring.
- servers: List of initial server names (e.g., ["S0", "S1", "S2"])
- num_replicas: Number of virtual nodes per server for better load balancing
"""
self.num_replicas = num_replicas # Number of virtual nodes per server
self.ring = {} # Hash ring storing virtual node mappings
self.sorted_keys = [] # Sorted list of hash values (positions) on the ring
self.servers = set() # Set of physical servers (used for tracking)
# Add each server to the hash ring
for server in servers:
self.add_server(server)
def _hash(self, key):
"""Computes a hash value for a given key using MD5."""
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_server(self, server):
"""
Adds a server to the hash ring along with its virtual nodes.
- Each virtual node is a different hash of the server ID to distribute load.
- The server is hashed multiple times and placed at different positions.
"""
self.servers.add(server)
for i in range(self.num_replicas): # Creating multiple virtual nodes
hash_val = self._hash(f"{server}-{i}") # Unique hash for each virtual node
self.ring[hash_val] = server # Map hash to the server
bisect.insort(self.sorted_keys, hash_val) # Maintain a sorted list for efficient lookup
def remove_server(self, server):
"""
Removes a server and all its virtual nodes from the hash ring.
"""
if server in self.servers:
self.servers.remove(server)
for i in range(self.num_replicas):
hash_val = self._hash(f"{server}-{i}") # Remove each virtual node's hash
self.ring.pop(hash_val, None) # Delete from hash ring
self.sorted_keys.remove(hash_val) # Remove from sorted key list
def get_server(self, key):
"""
Finds the closest server for a given key.
- Hash the key to get its position on the ring.
- Move clockwise to find the nearest server.
- If it exceeds the last node, wrap around to the first node.
"""
if not self.ring:
return None # No servers available
hash_val = self._hash(key) # Hash the key
index = bisect.bisect(self.sorted_keys, hash_val) % len(self.sorted_keys) # Locate nearest server
return self.ring[self.sorted_keys[index]] # Return the assigned server
# ----------------- Usage Example -------------------
# Step 1: Initialize Consistent Hashing with servers
servers = ["S0", "S1", "S2", "S3", "S4", "S5"]
ch = ConsistentHashing(servers)
# Step 2: Assign requests (keys) to servers
print(ch.get_server("UserA")) # Maps UserA to a server
print(ch.get_server("UserB")) # Maps UserB to a server
# Step 3: Add a new server dynamically
ch.add_server("S6")
print(ch.get_server("UserA")) # Might be reassigned if affected
# Step 4: Remove a server dynamically
ch.remove_server("S2")
print(ch.get_server("UserB")) # Might be reassigned if affected
Key Design Decisions
- MD5 Hashing: Both implementations use MD5 to distribute keys uniformly across the ring. MD5 is not cryptographically secure, but its distribution properties are excellent for hashing.
- Virtual Nodes: Without virtual nodes, 3 physical servers might hash to adjacent positions, leaving large gaps. With 100-200 virtual nodes per server, distribution is nearly uniform.
- TreeMap / Sorted List: Java uses TreeMap (red-black tree) for O(log n) ceiling lookups. Python uses bisect on a sorted list for the same complexity.
Production Considerations
- Thread Safety: The Java implementation is not thread-safe. In production, wrap mutations in synchronized blocks or use ConcurrentSkipListMap.
- Hash Function: For production, consider xxHash or MurmurHash3 — they are faster than MD5 while maintaining good distribution.
- Monitoring: Track the number of keys per node to detect imbalance. Alert if any node holds more than 2x its fair share.
- Rebalancing: When adding a node, only keys between the new node and its predecessor need to migrate. Implement this as a background process.
Related Interview Questions
- How does consistent hashing differ from modular hashing (key % N)?
- What problem do virtual nodes solve?
- How would you implement rebalancing when a new node is added?
- How is consistent hashing used in distributed caching (Memcached)?
Source
Code from System-Design-Overview by Ashish Pratap Singh. Explanations are original.
Original DosiBridge notes for Consistent Hashing Implementation
This section is written specifically for this page, not copied from a general system-design checklist. The useful way to study Consistent Hashing Implementation is to connect the concept to a production service where growth exposes the first weak assumption in the original design. That framing changes the design discussion from definitions to operating choices.
What makes this topic distinct
- Practice Loop: For Consistent Hashing Implementation, decide the practice loop before choosing tools; otherwise the design looks clean but fails when consistent changes.
- Implementation Exercise: For Consistent Hashing Implementation, decide the implementation exercise before choosing tools; otherwise the design looks clean but fails when hashing changes.
- Review Checkpoint: For Consistent Hashing Implementation, decide the review checkpoint before choosing tools; otherwise the design looks clean but fails when code changes.
- Failure Drill: For Consistent Hashing Implementation, decide the failure drill before choosing tools; otherwise the design looks clean but fails when consistent changes.
- Comparison Target: For Consistent Hashing Implementation, decide the comparison target before choosing tools; otherwise the design looks clean but fails when hashing changes.
Production review question
If a team adopts Consistent Hashing Implementation, what observable signal proves it helped instead of only making the architecture diagram more fashionable?
Practical metric to track
Track consistent-code health as a named dashboard or review item: combine one user-facing metric, one saturation metric, and one correctness metric. For Consistent Hashing Implementation, this keeps the discussion grounded in behavior that can be measured after deployment instead of generic advice that sounds good in an interview but never gets verified.