Common Load Balancing Algorithms and Their Java Implementations
This article introduces fundamental load balancing concepts and examines several static and dynamic algorithms—including round‑robin, weighted, smooth weighted round‑robin, consistent hashing, least‑active, and optimal‑response—providing Java code examples and discussing their advantages, disadvantages, and suitable use cases.
Load balancing is a core concept in high‑availability architectures, appearing in micro‑services, middleware, cloud computing, and big‑data stacks. The article first distinguishes static scheduling (fixed configuration) from dynamic scheduling (runtime metrics) and then explores a range of algorithms.
Basic Load Balancing Algorithms
Simple strategies such as round‑robin, random selection, and basic weighted distribution are presented with concise Java implementations.
public class Servers {
public static List
SERVERS = Arrays.asList(
"44.120.110.001:8080",
"44.120.110.002:8081",
"44.120.110.003:8082"
);
}
public class RoundRobin {
private static AtomicInteger requestIndex = new AtomicInteger(0);
public static String getServer() {
int index = requestIndex.get() % Servers.SERVERS.size();
requestIndex.incrementAndGet();
return Servers.SERVERS.get(index);
}
}The round‑robin implementation cycles through the server list using an atomic counter, ensuring even request distribution with minimal overhead.
Weighted Algorithms
Weight‑based strategies enhance basic algorithms by assigning higher traffic to more capable nodes. Two variants are shown: random weighted and round‑robin weighted.
public class RandomWeight {
static Map
WEIGHT_SERVERS = new LinkedHashMap<>();
static {
WEIGHT_SERVERS.put("44.120.110.001:8080", 17);
WEIGHT_SERVERS.put("44.120.110.002:8081", 11);
WEIGHT_SERVERS.put("44.120.110.003:8082", 30);
}
static Random random = new Random();
public static String getServer() {
int weightTotal = WEIGHT_SERVERS.values().stream().mapToInt(Integer::intValue).sum();
int index = random.nextInt(weightTotal);
for (Map.Entry
e : WEIGHT_SERVERS.entrySet()) {
if (index < e.getValue()) return e.getKey();
index -= e.getValue();
}
return null;
}
}These implementations calculate the total weight, generate a random offset, and select the server whose cumulative weight covers that offset.
Smooth Weighted Round‑Robin
The smooth weighted round‑robin algorithm solves the uneven distribution problem of the naive weighted round‑robin by maintaining a dynamic weight for each node that is adjusted on every request.
public class Weight {
private String server;
private int weight;
private int currentWeight;
// constructors, getters, setters omitted for brevity
}
public class SmoothRoundRobin {
private static Map
weightMap = new HashMap<>();
private static int weightTotal = 0;
static { // initialization
for (Map.Entry
e : Servers.WEIGHT_SERVERS.entrySet()) {
weightMap.put(e.getKey(), new Weight(e.getKey(), e.getValue(), 0));
weightTotal += e.getValue();
}
}
public static String getServer() {
for (Weight w : weightMap.values()) {
w.setCurrentWeight(w.getCurrentWeight() + w.getWeight());
}
Weight max = Collections.max(weightMap.values(), Comparator.comparingInt(Weight::getCurrentWeight));
max.setCurrentWeight(max.getCurrentWeight() - weightTotal);
return max.getServer();
}
}This algorithm guarantees that over a full cycle each server receives requests proportional to its weight, while distributing them as evenly as possible.
Consistent Hashing
Consistent hashing maps both servers and keys onto a logical ring (2³² points). A request is routed to the first server encountered clockwise from the key’s position, providing stable mapping even when nodes join or leave.
public class ConsistentHash {
private static final int VIRTUAL_NODES = 160;
private static TreeMap
virtualNodes = new TreeMap<>();
static {
for (String ip : Servers.SERVERS) {
virtualNodes.put(hash(ip), ip);
for (int i = 0; i < VIRTUAL_NODES; i++) {
virtualNodes.put(hash(ip + i), ip);
}
}
}
public static String getServer(String key) {
int hash = hash(key);
SortedMap
tail = virtualNodes.tailMap(hash);
int nodeKey = tail.isEmpty() ? virtualNodes.firstKey() : tail.firstKey();
return virtualNodes.get(nodeKey);
}
private static int hash(String s) { /* simple custom hash */ }
}Virtual nodes improve distribution uniformity and reduce hotspot risk.
Dynamic Algorithms
Two adaptive strategies are covered:
Least‑Active : tracks the number of in‑flight requests per server and selects the one with the smallest count, periodically decaying the counters.
Optimal‑Response : issues parallel ping probes to all servers (using CompletableFuture ) and chooses the fastest responder for the actual request.
public class LeastActive {
public static String getServer() {
return Servers.SERVERS.stream()
.min(Comparator.comparingInt(s -> s.getActive().get()))
.map(Server::getIP)
.orElse(null);
}
}
public class ResponseTime {
private static ExecutorService pool = Executors.newFixedThreadPool(Servers.SERVERS.size());
public static String getServer() throws InterruptedException {
List
> futures = Servers.SERVERS.stream()
.map(s -> CompletableFuture.supplyAsync(s::ping, pool))
.collect(Collectors.toList());
return CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]))
.thenApply(ip -> (String) ip).join();
}
}These methods adapt to real‑time load and latency, offering better performance in heterogeneous environments.
Conclusion
The article compares static and dynamic load‑balancing techniques, highlighting trade‑offs between simplicity (e.g., round‑robin) and intelligence (e.g., smooth weighted or optimal‑response). In high‑throughput scenarios, the simplest algorithm often yields the best overall performance when server capacities are uniform.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.