Common Load Balancing Algorithms and Their Java Implementations

This article explains the principles behind popular load‑balancing strategies such as round‑robin, weighted round‑robin, smooth weighted round‑robin, consistent hashing, least‑active, and best‑response, and provides clear Java code examples for each method.

Top Architect
Top Architect
Top Architect
Common Load Balancing Algorithms and Their Java Implementations

Load balancing is essential for high‑availability systems. This article introduces several common load‑balancing strategies, explains their principles, and provides Java implementations.

Basic static algorithms

Round‑Robin distributes requests sequentially across servers. Random selects a server at random. Both are simple but ignore server capacity.

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);
    }
}

Weighted algorithms

Weighted Round‑Robin and Random assign a weight to each server, allowing more powerful nodes to receive more traffic. Simple implementations use a weight map.

public class WeightedRoundRobin {
    private static AtomicInteger requestCount = new AtomicInteger(0);
    public static String getServer() {
        int totalWeight = Servers.WEIGHT_SERVERS.values().stream().mapToInt(Integer::intValue).sum();
        int index = requestCount.get() % totalWeight;
        requestCount.incrementAndGet();
        for (Map.Entry<String,Integer> e : Servers.WEIGHT_SERVERS.entrySet()) {
            if (index < e.getValue()) return e.getKey();
            index -= e.getValue();
        }
        return null;
    }
}

Smooth Weighted Round‑Robin

This algorithm updates a dynamic weight for each server on every request, guaranteeing a smooth distribution that respects the configured weights.

public class SmoothWeightedRoundRobin {
    private static Map<String,Weight> weightMap = new HashMap<>();
    static {
        Servers.WEIGHT_SERVERS.forEach((s,w) -> weightMap.put(s,new Weight(s,w,0)));
    }
    public static String getServer() {
        weightMap.values().forEach(v -> v.currentWeight += v.weight);
        Weight max = Collections.max(weightMap.values(), Comparator.comparingInt(v -> v.currentWeight));
        max.currentWeight -= weightMap.values().stream().mapToInt(v -> v.weight).sum();
        return max.server;
    }
}

Consistent Hashing

Consistent hashing maps both servers and request keys onto a virtual ring; a request is routed to the first server clockwise from its hash. Virtual nodes improve distribution and resilience to node changes.

public class ConsistentHash {
    private static TreeMap<Integer,String> ring = new TreeMap<>();
    static {
        for (String server : Servers.SERVERS) {
            ring.put(hash(server), server);
            for (int i=0;i<VIRTUAL_NODES;i++) {
                ring.put(hash(server+"#"+i), server);
            }
        }
    }
    public static String getServer(String key) {
        int h = hash(key);
        SortedMap<Integer,String> tail = ring.tailMap(h);
        return tail.isEmpty() ? ring.firstEntry().getValue() : tail.firstEntry().getValue();
    }
    private static int hash(String s) { /* simple hash */ }
}

Dynamic algorithms

Least‑Active selects the server with the fewest ongoing requests, while the Best‑Response (or Optimal Response) algorithm probes servers and chooses the one with the lowest latency.

public class LeastActive {
    public static String getServer() {
        return Servers.SERVERS.stream()
            .min(Comparator.comparingInt(s -> s.getActive().get()))
            .orElseThrow().getIP();
    }
}
public class BestResponse {
    private static ExecutorService pool = Executors.newFixedThreadPool(Servers.SERVERS.size());
    public static String getServer() throws InterruptedException {
        List<CompletableFuture<String>> 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).get();
    }
}

Conclusion

Static algorithms are fast and suitable when all nodes have similar capacity; dynamic algorithms provide better load distribution under varying conditions but incur higher overhead. Choosing the right strategy depends on workload characteristics and performance requirements.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDistributed Systemsload balancingAlgorithms
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.