Scalability Patterns trong Java
1. Horizontal Scaling
1.1 Load Balancing
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class ServiceDiscoveryClient {
private final RestTemplate restTemplate;
public Response callService(String serviceId, String path) {
return restTemplate.getForObject(
"http://" + serviceId + path,
Response.class
);
}
}
1.2 Session Management
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
}
@RestController
@SessionAttributes("user")
public class UserController {
@GetMapping("/api/session")
public User getUserFromSession(
@ModelAttribute("user") User user) {
return user;
}
}
2. Database Scaling
2.1 Read Replicas
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.write")
public DataSource writeDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.read")
public DataSource readDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public AbstractRoutingDataSource routingDataSource(
@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.WRITE, writeDataSource);
targetDataSources.put(DataSourceType.READ, readDataSource);
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? DataSourceType.READ
: DataSourceType.WRITE;
}
};
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(writeDataSource);
return routingDataSource;
}
}
2.2 Database Sharding
@Configuration
public class ShardingConfig {
@Bean
public ShardingDataSource shardingDataSource() {
Map<String, DataSource> dataSourceMap = createDataSourceMap();
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
// Configure table rules
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration();
orderTableRuleConfig.setLogicTable("orders");
orderTableRuleConfig.setActualDataNodes("ds${0..1}.orders_${0..1}");
// Configure sharding strategy
orderTableRuleConfig.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration(
"order_id",
new PreciseOrderShardingAlgorithm()
)
);
shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);
return new ShardingDataSource(dataSourceMap, shardingRuleConfig);
}
}
public class PreciseOrderShardingAlgorithm
implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(
Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
for (String each : availableTargetNames) {
if (each.endsWith(shardingValue.getValue() % 2 + "")) {
return each;
}
}
throw new UnsupportedOperationException();
}
}
3. Caching Strategies
3.1 Multi-level Caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
// First level: Caffeine (Local Cache)
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100));
// Second level: Redis (Distributed Cache)
RedisCacheManager redisCacheManager = RedisCacheManager
.builder(redisConnectionFactory())
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)))
.build();
compositeCacheManager.setCacheManagers(Arrays.asList(
caffeineCacheManager,
redisCacheManager
));
return compositeCacheManager;
}
}
@Service
public class UserService {
@Cacheable(value = "users", unless = "#result == null")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
}
3.2 Cache Synchronization
@Service
public class CacheEventHandler {
private final CacheManager cacheManager;
@KafkaListener(topics = "cache-events")
public void handleCacheEvent(CacheEvent event) {
switch (event.getType()) {
case EVICT:
cacheManager.getCache(event.getCacheName())
.evict(event.getKey());
break;
case UPDATE:
cacheManager.getCache(event.getCacheName())
.put(event.getKey(), event.getValue());
break;
}
}
}
4. Asynchronous Processing
4.1 Message Queues
@Configuration
public class AsyncConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class OrderProcessor {
@Async
public CompletableFuture<Order> processOrder(Order order) {
return CompletableFuture.supplyAsync(() -> {
// Process order asynchronously
return processOrderDetails(order);
}).thenApply(processedOrder -> {
// Post-processing
return finalizeOrder(processedOrder);
});
}
}
4.2 Event Streaming
@Configuration
public class KafkaStreamConfig {
@Bean
public StreamsBuilder streamsBuilder() {
StreamsBuilder builder = new StreamsBuilder();
builder.stream("orders")
.groupByKey()
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)))
.aggregate(
() -> new OrderAggregate(),
(key, value, aggregate) -> aggregate.add(value),
Materialized.as("order-stats")
);
return builder;
}
}
5. Microservices Scaling
5.1 Service Discovery
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRegistryApplication.class, args);
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class MicroserviceApplication {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
5.2 Circuit Breaker
@Configuration
public class ResilienceConfig {
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowSize(2)
.build();
}
@Bean
public TimeLimiterConfig timeLimiterConfig() {
return TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build();
}
}
@Service
public class OrderService {
private final CircuitBreaker circuitBreaker;
private final TimeLimiter timeLimiter;
public Order getOrder(Long id) {
return circuitBreaker.executeSupplier(() ->
timeLimiter.executeFutureSupplier(() ->
CompletableFuture.supplyAsync(() ->
orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id))
)
)
);
}
}
6. API Gateway Scaling
6.1 Rate Limiting
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest()
.getHeaders()
.getFirst("X-API-Key")
);
}
@Bean
public RateLimiter rateLimiter() {
return RedisRateLimiter.builder()
.replenishRate(10)
.burstCapacity(20)
.build();
}
}
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(
RouteLocatorBuilder builder,
RateLimiter rateLimiter) {
return builder.routes()
.route("rate_limited_route", r -> r
.path("/api/**")
.filters(f -> f.requestRateLimiter()
.rateLimiter(rateLimiter)
.configure(c -> c.setKeyResolver(userKeyResolver())))
.uri("lb://api-service"))
.build();
}
}
6.2 Request Buffering
@Configuration
public class WebFluxConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.filter(new BufferingWebFilter())
.build();
}
}
public class BufferingWebFilter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(
ClientRequest request,
ExchangeFunction next) {
return next.exchange(request)
.map(response -> response.mutate()
.body(response.body(BodyExtractors.toDataBuffers())
.buffer()
.map(dataBuffer -> {
// Buffer and process response
return dataBuffer;
}))
.build());
}
}
7. Monitoring và Metrics
@Configuration
public class MetricsConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "scalability-demo");
}
}
@Component
public class PerformanceMonitor {
private final MeterRegistry registry;
public void recordLatency(String operation, long duration) {
registry.timer("operation.latency",
"operation", operation)
.record(duration, TimeUnit.MILLISECONDS);
}
public void incrementCounter(String operation) {
registry.counter("operation.count",
"operation", operation)
.increment();
}
}
7.2 Health Checks
@Component
public class CustomHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Builder builder) throws Exception {
try {
// Perform health check
Map<String, Object> details = new HashMap<>();
details.put("version", "1.0.0");
details.put("timestamp", System.currentTimeMillis());
builder.up()
.withDetails(details);
} catch (Exception e) {
builder.down()
.withException(e);
}
}
}
8. Best Practices
8.1 Design Guidelines
- Stateless services
- Eventual consistency
- Fail fast and independently
- Cache strategically
- Monitor and alert
- Automated scaling
- Data partitioning
- Asynchronous processing
8.2 Implementation Guidelines
- Use connection pooling
- Implement retry mechanisms
- Handle partial failures
- Use appropriate serialization
- Implement proper logging
- Use appropriate timeouts
- Implement circuit breakers
- Use appropriate caching strategies
9. References
- Java Concurrency in Practice by Brian Goetz
- Designing Data-Intensive Applications by Martin Kleppmann
- Building Microservices by Sam Newman
- Cloud Native Java by Josh Long & Kenny Bastani