Read Replicas Load Balancing in Java

Introduction

Read replicas load balancing distributes database read queries across multiple replica servers to improve performance, increase availability, and scale read operations. This implementation provides a comprehensive solution for managing read replicas in Java applications.

Architecture Overview

System Architecture

public class ReadReplicaLoadBalancer {
/**
* Read Replica Load Balancing Components:
* 1. Replica Discovery & Health Monitoring
* 2. Load Balancing Strategies (Round Robin, Weighted, Least Connections)
* 3. Connection Pool Management
* 4. Metrics & Monitoring
* 5. Failover & Recovery
*/
private ReplicaManager replicaManager;
private LoadBalancingStrategy strategy;
private HealthMonitor healthMonitor;
private MetricsCollector metricsCollector;
public ReadReplicaLoadBalancer(LoadBalancingStrategy strategy) {
this.replicaManager = new ReplicaManager();
this.strategy = strategy;
this.healthMonitor = new HealthMonitor();
this.metricsCollector = new MetricsCollector();
}
public Connection getReadConnection() throws SQLException {
DataSource replica = strategy.selectReplica(replicaManager.getHealthyReplicas());
return replica.getConnection();
}
public void executeQuery(String sql, ResultSetHandler handler) {
try (Connection conn = getReadConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
ResultSet rs = stmt.executeQuery();
handler.handleResult(rs);
} catch (SQLException e) {
metricsCollector.recordError();
throw new RuntimeException("Query execution failed", e);
}
}
}

Core Components

Replica Configuration Management

public class ReplicaConfig {
private final String id;
private final String url;
private final String username;
private final String password;
private final int weight;
private final boolean enabled;
private final Map<String, String> properties;
public ReplicaConfig(String id, String url, String username, String password, 
int weight, boolean enabled) {
this.id = id;
this.url = url;
this.username = username;
this.password = password;
this.weight = weight;
this.enabled = enabled;
this.properties = new HashMap<>();
}
// Builder pattern for easy configuration
public static class Builder {
private String id;
private String url;
private String username;
private String password;
private int weight = 1;
private boolean enabled = true;
public Builder id(String id) { this.id = id; return this; }
public Builder url(String url) { this.url = url; return this; }
public Builder username(String username) { this.username = username; return this; }
public Builder password(String password) { this.password = password; return this; }
public Builder weight(int weight) { this.weight = weight; return this; }
public Builder enabled(boolean enabled) { this.enabled = enabled; return this; }
public ReplicaConfig build() {
return new ReplicaConfig(id, url, username, password, weight, enabled);
}
}
// Getters
public String getId() { return id; }
public String getUrl() { return url; }
public int getWeight() { return weight; }
public boolean isEnabled() { return enabled; }
}
@Component
public class ReplicaManager {
private final Map<String, ReplicaConfig> replicaConfigs;
private final Map<String, DataSource> replicaDataSources;
private final List<ReplicaConfig> healthyReplicas;
private final HealthMonitor healthMonitor;
public ReplicaManager() {
this.replicaConfigs = new ConcurrentHashMap<>();
this.replicaDataSources = new ConcurrentHashMap<>();
this.healthyReplicas = new CopyOnWriteArrayList<>();
this.healthMonitor = new HealthMonitor();
}
public void addReplica(ReplicaConfig config) {
replicaConfigs.put(config.getId(), config);
DataSource dataSource = createDataSource(config);
replicaDataSources.put(config.getId(), dataSource);
// Start health monitoring
healthMonitor.monitorReplica(config, dataSource);
}
public void removeReplica(String replicaId) {
replicaConfigs.remove(replicaId);
replicaDataSources.remove(replicaId);
healthyReplicas.removeIf(config -> config.getId().equals(replicaId));
}
public List<ReplicaConfig> getHealthyReplicas() {
return new ArrayList<>(healthyReplicas);
}
public DataSource getDataSource(String replicaId) {
return replicaDataSources.get(replicaId);
}
public void updateReplicaHealth(String replicaId, boolean healthy) {
ReplicaConfig config = replicaConfigs.get(replicaId);
if (config != null) {
if (healthy && !healthyReplicas.contains(config)) {
healthyReplicas.add(config);
} else if (!healthy) {
healthyReplicas.remove(config);
}
}
}
private DataSource createDataSource(ReplicaConfig config) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(config.getUrl());
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setMaximumPoolSize(10);
dataSource.setMinimumIdle(2);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(600000);
dataSource.setMaxLifetime(1800000);
return dataSource;
}
}

Health Monitoring

@Component
public class HealthMonitor {
private final ScheduledExecutorService scheduler;
private final Map<String, ScheduledFuture<?>> monitoringTasks;
private final ReplicaManager replicaManager;
private final MetricsCollector metricsCollector;
public HealthMonitor() {
this.scheduler = Executors.newScheduledThreadPool(2);
this.monitoringTasks = new ConcurrentHashMap<>();
this.replicaManager = new ReplicaManager(); // In real app, inject this
this.metricsCollector = new MetricsCollector();
}
public void monitorReplica(ReplicaConfig config, DataSource dataSource) {
ScheduledFuture<?> task = scheduler.scheduleAtFixedRate(
() -> checkReplicaHealth(config, dataSource),
0, 30, TimeUnit.SECONDS); // Check every 30 seconds
monitoringTasks.put(config.getId(), task);
}
public void stopMonitoring(String replicaId) {
ScheduledFuture<?> task = monitoringTasks.remove(replicaId);
if (task != null) {
task.cancel(false);
}
}
private void checkReplicaHealth(ReplicaConfig config, DataSource dataSource) {
boolean isHealthy = performHealthCheck(dataSource);
replicaManager.updateReplicaHealth(config.getId(), isHealthy);
if (isHealthy) {
metricsCollector.recordHealthCheckSuccess(config.getId());
} else {
metricsCollector.recordHealthCheckFailure(config.getId());
}
}
private boolean performHealthCheck(DataSource dataSource) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// Simple health check query
ResultSet rs = stmt.executeQuery("SELECT 1");
return rs.next() && rs.getInt(1) == 1;
} catch (SQLException e) {
return false;
}
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

Load Balancing Strategies

Strategy Interface

public interface LoadBalancingStrategy {
DataSource selectReplica(List<ReplicaConfig> healthyReplicas);
String getStrategyName();
}
// Round Robin Strategy
@Component
public class RoundRobinStrategy implements LoadBalancingStrategy {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
int index = Math.abs(counter.getAndIncrement() % healthyReplicas.size());
ReplicaConfig selected = healthyReplicas.get(index);
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "RoundRobin";
}
private DataSource getDataSource(ReplicaConfig config) {
// Implementation to get DataSource from ReplicaManager
return null;
}
}
// Weighted Round Robin Strategy
@Component
public class WeightedRoundRobinStrategy implements LoadBalancingStrategy {
private final AtomicInteger counter = new AtomicInteger(0);
private final List<ReplicaConfig> weightedReplicas = new ArrayList<>();
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
updateWeightedList(healthyReplicas);
int index = Math.abs(counter.getAndIncrement() % weightedReplicas.size());
ReplicaConfig selected = weightedReplicas.get(index);
return getDataSource(selected);
}
private void updateWeightedList(List<ReplicaConfig> healthyReplicas) {
weightedReplicas.clear();
for (ReplicaConfig config : healthyReplicas) {
for (int i = 0; i < config.getWeight(); i++) {
weightedReplicas.add(config);
}
}
}
@Override
public String getStrategyName() {
return "WeightedRoundRobin";
}
}
// Least Connections Strategy
@Component
public class LeastConnectionsStrategy implements LoadBalancingStrategy {
private final ConnectionTracker connectionTracker;
public LeastConnectionsStrategy(ConnectionTracker connectionTracker) {
this.connectionTracker = connectionTracker;
}
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
ReplicaConfig selected = healthyReplicas.stream()
.min(Comparator.comparingInt(connectionTracker::getActiveConnections))
.orElse(healthyReplicas.get(0));
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "LeastConnections";
}
}
// Latency-Based Strategy
@Component
public class LatencyBasedStrategy implements LoadBalancingStrategy {
private final LatencyMonitor latencyMonitor;
public LatencyBasedStrategy(LatencyMonitor latencyMonitor) {
this.latencyMonitor = latencyMonitor;
}
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
ReplicaConfig selected = healthyReplicas.stream()
.min(Comparator.comparingDouble(latencyMonitor::getAverageLatency))
.orElse(healthyReplicas.get(0));
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "LatencyBased";
}
}

Connection Tracking

@Component
public class ConnectionTracker {
private final Map<String, AtomicInteger> activeConnections = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> totalConnections = new ConcurrentHashMap<>();
public void connectionAcquired(String replicaId) {
activeConnections.computeIfAbsent(replicaId, k -> new AtomicInteger(0))
.incrementAndGet();
totalConnections.computeIfAbsent(replicaId, k -> new AtomicLong(0))
.incrementAndGet();
}
public void connectionReleased(String replicaId) {
AtomicInteger counter = activeConnections.get(replicaId);
if (counter != null) {
counter.decrementAndGet();
}
}
public int getActiveConnections(ReplicaConfig config) {
AtomicInteger counter = activeConnections.get(config.getId());
return counter != null ? counter.get() : 0;
}
public long getTotalConnections(ReplicaConfig config) {
AtomicLong counter = totalConnections.get(config.getId());
return counter != null ? counter.get() : 0;
}
public Map<String, Integer> getActiveConnectionsSnapshot() {
return activeConnections.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
}
}

Metrics and Monitoring

Comprehensive Metrics Collection

@Component
public class MetricsCollector {
private final MeterRegistry meterRegistry;
private final Map<String, Counter> errorCounters = new ConcurrentHashMap<>();
private final Map<String, Timer> queryTimers = new ConcurrentHashMap<>();
private final Map<String, Gauge> connectionGauges = new ConcurrentHashMap<>();
public MetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordQueryExecution(String replicaId, long duration, boolean success) {
Timer timer = queryTimers.computeIfAbsent(replicaId, 
id -> Timer.builder("database.query.duration")
.tag("replica", id)
.register(meterRegistry));
timer.record(duration, TimeUnit.MILLISECONDS);
if (!success) {
Counter counter = errorCounters.computeIfAbsent(replicaId,
id -> Counter.builder("database.query.errors")
.tag("replica", id)
.register(meterRegistry));
counter.increment();
}
}
public void recordHealthCheckSuccess(String replicaId) {
Counter.builder("database.health.check")
.tag("replica", replicaId)
.tag("status", "success")
.register(meterRegistry)
.increment();
}
public void recordHealthCheckFailure(String replicaId) {
Counter.builder("database.health.check")
.tag("replica", replicaId)
.tag("status", "failure")
.register(meterRegistry)
.increment();
}
public void registerConnectionGauge(String replicaId, Supplier<Number> connectionSupplier) {
Gauge.builder("database.connections.active")
.tag("replica", replicaId)
.register(meterRegistry, connectionSupplier);
}
public void recordReplicaSelection(String strategy, String replicaId) {
Counter.builder("loadbalancer.selection")
.tag("strategy", strategy)
.tag("replica", replicaId)
.register(meterRegistry)
.increment();
}
}
@Component
public class LatencyMonitor {
private final Map<String, CircularBuffer<Long>> latencyBuffers = new ConcurrentHashMap<>();
private final int bufferSize = 100;
public void recordLatency(String replicaId, long latencyMs) {
CircularBuffer<Long> buffer = latencyBuffers.computeIfAbsent(replicaId,
id -> new CircularBuffer<>(bufferSize));
buffer.add(latencyMs);
}
public double getAverageLatency(ReplicaConfig config) {
CircularBuffer<Long> buffer = latencyBuffers.get(config.getId());
if (buffer == null || buffer.size() == 0) {
return 0.0;
}
return buffer.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0.0);
}
public double get95thPercentileLatency(ReplicaConfig config) {
CircularBuffer<Long> buffer = latencyBuffers.get(config.getId());
if (buffer == null || buffer.size() == 0) {
return 0.0;
}
List<Long> sorted = buffer.stream().sorted().collect(Collectors.toList());
int index = (int) Math.ceil(0.95 * sorted.size()) - 1;
return sorted.get(Math.max(0, index));
}
}
// Circular buffer implementation for latency tracking
public class CircularBuffer<T> {
private final T[] buffer;
private int head = 0;
private int tail = 0;
private int size = 0;
@SuppressWarnings("unchecked")
public CircularBuffer(int capacity) {
this.buffer = (T[]) new Object[capacity];
}
public void add(T item) {
buffer[tail] = item;
tail = (tail + 1) % buffer.length;
if (size < buffer.length) {
size++;
} else {
head = (head + 1) % buffer.length;
}
}
public Stream<T> stream() {
return IntStream.range(0, size)
.mapToObj(i -> buffer[(head + i) % buffer.length]);
}
public int size() {
return size;
}
}

Spring Boot Integration

Configuration Classes

@Configuration
@EnableConfigurationProperties(ReplicaProperties.class)
public class ReadReplicaConfig {
@Bean
@Primary
public DataSource routingDataSource(ReplicaProperties properties) {
Map<Object, Object> targetDataSources = new HashMap<>();
// Primary write data source
targetDataSources.put("primary", createDataSource(properties.getPrimary()));
// Read replicas
properties.getReplicas().forEach((id, config) -> {
targetDataSources.put(id, createDataSource(config));
});
ReadWriteRoutingDataSource routingDataSource = new ReadWriteRoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(targetDataSources.get("primary"));
return routingDataSource;
}
@Bean
public ReplicaManager replicaManager(ReplicaProperties properties) {
ReplicaManager manager = new ReplicaManager();
properties.getReplicas().forEach((id, config) -> {
manager.addReplica(new ReplicaConfig.Builder()
.id(id)
.url(config.getUrl())
.username(config.getUsername())
.password(config.getPassword())
.weight(config.getWeight())
.enabled(config.isEnabled())
.build());
});
return manager;
}
@Bean
public LoadBalancingStrategy loadBalancingStrategy(ReplicaProperties properties) {
String strategy = properties.getLoadBalancing().getStrategy();
switch (strategy.toLowerCase()) {
case "roundrobin":
return new RoundRobinStrategy();
case "weighted":
return new WeightedRoundRobinStrategy();
case "leastconnections":
return new LeastConnectionsStrategy(new ConnectionTracker());
case "latency":
return new LatencyBasedStrategy(new LatencyMonitor());
default:
return new RoundRobinStrategy();
}
}
@Bean
public ReadReplicaLoadBalancer readReplicaLoadBalancer(
ReplicaManager replicaManager,
LoadBalancingStrategy strategy) {
return new ReadReplicaLoadBalancer(strategy);
}
private DataSource createDataSource(ReplicaProperties.DataSourceConfig config) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(config.getUrl());
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setDriverClassName(config.getDriverClassName());
dataSource.setMaximumPoolSize(config.getMaxPoolSize());
dataSource.setMinimumIdle(config.getMinIdle());
dataSource.setConnectionTimeout(config.getConnectionTimeout());
return dataSource;
}
}
@ConfigurationProperties(prefix = "database.read-replicas")
@Data
public class ReplicaProperties {
private DataSourceConfig primary;
private Map<String, DataSourceConfig> replicas = new HashMap<>();
private LoadBalancing loadBalancing = new LoadBalancing();
@Data
public static class DataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName = "com.mysql.cj.jdbc.Driver";
private int maxPoolSize = 10;
private int minIdle = 2;
private long connectionTimeout = 30000;
private int weight = 1;
private boolean enabled = true;
}
@Data
public static class LoadBalancing {
private String strategy = "roundrobin";
private int healthCheckInterval = 30;
private int connectionTimeout = 5000;
}
}

Routing Data Source

public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<Boolean> readOnlyContext = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
Boolean readOnly = readOnlyContext.get();
if (readOnly != null && readOnly) {
return "read"; // This will be resolved by the load balancer
}
return "primary";
}
public static void setReadOnly(boolean readOnly) {
readOnlyContext.set(readOnly);
}
public static void clearContext() {
readOnlyContext.remove();
}
}
@Component
public class ReadReplicaAspect {
private final ReadReplicaLoadBalancer loadBalancer;
public ReadReplicaAspect(ReadReplicaLoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer;
}
@Around("@annotation(readOnly)")
public Object routeToReadReplica(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
ReadWriteRoutingDataSource.setReadOnly(true);
try {
return joinPoint.proceed();
} finally {
ReadWriteRoutingDataSource.clearContext();
}
}
@Around("execution(* org.springframework.data.repository.Repository+.*(..)) && " +
"@annotation(org.springframework.transaction.annotation.Transactional) && " +
"!@annotation(org.springframework.transaction.annotation.Transactional(readOnly = false))")
public Object routeReadTransactions(ProceedingJoinPoint joinPoint) throws Throwable {
ReadWriteRoutingDataSource.setReadOnly(true);
try {
return joinPoint.proceed();
} finally {
ReadWriteRoutingDataSource.clearContext();
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

Usage Examples

Service Layer Implementation

@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final ReadReplicaLoadBalancer loadBalancer;
public UserService(UserRepository userRepository, ReadReplicaLoadBalancer loadBalancer) {
this.userRepository = userRepository;
this.loadBalancer = loadBalancer;
}
// Write operation - goes to primary
public User createUser(User user) {
return userRepository.save(user);
}
// Read operation - goes to read replica
@ReadOnly
@Transactional(readOnly = true)
public User findUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// Complex read operation with custom SQL
@ReadOnly
public List<User> findActiveUsers() {
String sql = "SELECT * FROM users WHERE status = 'ACTIVE'";
List<User> users = new ArrayList<>();
loadBalancer.executeQuery(sql, resultSet -> {
while (resultSet.next()) {
users.add(mapResultSetToUser(resultSet));
}
});
return users;
}
// Batch read operations
@ReadOnly
public Map<Long, User> findUsersByIds(List<Long> userIds) {
String sql = "SELECT * FROM users WHERE id IN (" + 
userIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")";
Map<Long, User> users = new HashMap<>();
loadBalancer.executeQuery(sql, resultSet -> {
while (resultSet.next()) {
User user = mapResultSetToUser(resultSet);
users.put(user.getId(), user);
}
});
return users;
}
private User mapResultSetToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setStatus(rs.getString("status"));
return user;
}
}

Monitoring and Administration

Admin Endpoints

@RestController
@RequestMapping("/admin/read-replicas")
public class ReadReplicaAdminController {
private final ReplicaManager replicaManager;
private final HealthMonitor healthMonitor;
private final MetricsCollector metricsCollector;
public ReadReplicaAdminController(ReplicaManager replicaManager, 
HealthMonitor healthMonitor,
MetricsCollector metricsCollector) {
this.replicaManager = replicaManager;
this.healthMonitor = healthMonitor;
this.metricsCollector = metricsCollector;
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getReplicaStatus() {
Map<String, Object> status = new HashMap<>();
List<ReplicaConfig> healthyReplicas = replicaManager.getHealthyReplicas();
status.put("healthyReplicas", healthyReplicas.size());
status.put("totalReplicas", replicaManager.getReplicaCount());
status.put("replicas", getDetailedReplicaInfo());
return ResponseEntity.ok(status);
}
@PostMapping("/{replicaId}/enable")
public ResponseEntity<Void> enableReplica(@PathVariable String replicaId) {
// Implementation to enable a replica
return ResponseEntity.ok().build();
}
@PostMapping("/{replicaId}/disable")
public ResponseEntity<Void> disableReplica(@PathVariable String replicaId) {
// Implementation to disable a replica
return ResponseEntity.ok().build();
}
@GetMapping("/metrics")
public ResponseEntity<Map<String, Object>> getMetrics() {
Map<String, Object> metrics = new HashMap<>();
// Add various metrics
return ResponseEntity.ok(metrics);
}
private List<Map<String, Object>> getDetailedReplicaInfo() {
// Implementation to get detailed replica information
return Collections.emptyList();
}
}

Testing

Comprehensive Test Suite

@SpringBootTest
@TestPropertySource(properties = {
"database.read-replicas.primary.url=jdbc:h2:mem:primary;DB_CLOSE_DELAY=-1",
"database.read-replicas.replicas.replica1.url=jdbc:h2:mem:replica1;DB_CLOSE_DELAY=-1",
"database.read-replicas.replicas.replica2.url=jdbc:h2:mem:replica2;DB_CLOSE_DELAY=-1",
"database.read-replicas.load-balancing.strategy=roundrobin"
})
public class ReadReplicaLoadBalancerTest {
@Autowired
private ReadReplicaLoadBalancer loadBalancer;
@Autowired
private ReplicaManager replicaManager;
@Test
void testRoundRobinSelection() throws SQLException {
// Get multiple connections and verify round-robin distribution
Set<String> selectedReplicas = new HashSet<>();
for (int i = 0; i < 10; i++) {
try (Connection conn = loadBalancer.getReadConnection()) {
String url = conn.getMetaData().getURL();
selectedReplicas.add(url);
}
}
// Should have used multiple replicas
assertTrue(selectedReplicas.size() > 1);
}
@Test
void testNoHealthyReplicas() {
// Disable all replicas
replicaManager.getHealthyReplicas().forEach(config -> 
replicaManager.updateReplicaHealth(config.getId(), false));
assertThrows(NoHealthyReplicasException.class, () -> {
loadBalancer.getReadConnection();
});
}
@Test
void testQueryExecution() {
String sql = "SELECT 1";
loadBalancer.executeQuery(sql, resultSet -> {
assertTrue(resultSet.next());
assertEquals(1, resultSet.getInt(1));
});
}
}

Performance Optimization

Connection Pool Tuning

@Configuration
public class ConnectionPoolConfig {
@Bean
@ConfigurationProperties("database.read-replicas.connection-pool")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
public HikariDataSource primaryDataSource() {
HikariConfig config = hikariConfig();
config.setPoolName("PrimaryPool");
config.setReadOnly(false);
return new HikariDataSource(config);
}
@Bean
public HikariDataSource replicaDataSource(ReplicaProperties properties) {
HikariConfig config = hikariConfig();
config.setPoolName("ReplicaPool");
config.setReadOnly(true);
// Additional replica-specific configuration
return new HikariDataSource(config);
}
}
// Custom connection pool properties
@Data
@ConfigurationProperties("database.read-replicas.connection-pool")
public class ConnectionPoolProperties {
private int maximumPoolSize = 20;
private int minimumIdle = 5;
private long connectionTimeout = 30000;
private long idleTimeout = 600000;
private long maxLifetime = 1800000;
private String connectionTestQuery = "SELECT 1";
private boolean autoCommit = true;
}

Conclusion

This comprehensive read replica load balancing solution provides:

  1. Multiple load balancing strategies (Round Robin, Weighted, Least Connections, Latency-based)
  2. Health monitoring with automatic failover
  3. Comprehensive metrics and monitoring
  4. Spring Boot integration with easy configuration
  5. Connection pooling optimization
  6. Annotation-based routing for read/write separation
  7. Admin endpoints for management and monitoring

The system ensures high availability, improved read performance, and automatic failover while providing observability and easy management through comprehensive monitoring and administration capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper