Scaling Patterns and Architectural Strategies
A comprehensive guide to architectural patterns and strategies for building scalable applications.
Scaling Patterns and Architectural Strategies
Understanding the right architectural patterns is crucial for building scalable applications. Here are the most important patterns and strategies.
1. Microservices Architecture
What is Microservices?
Breaking down a large application into smaller, independent services that communicate over the network.
Benefits:
- Independent Deployment: Deploy services separately
- Technology Diversity: Use different technologies for different services
- Fault Isolation: One service failure doesn’t bring down the entire system
- Scalability: Scale individual services based on demand
Implementation Example:
// User Service
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
// Order Service
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id);
}
}
// Service Communication
@Service
public class OrderService {
@Autowired
private UserServiceClient userServiceClient;
public OrderWithUser getOrderWithUser(Long orderId) {
Order order = findById(orderId);
User user = userServiceClient.getUser(order.getUserId());
return new OrderWithUser(order, user);
}
}
Service Discovery:
# Eureka Server Configuration
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
2. Event-Driven Architecture
What is Event-Driven Architecture?
A pattern where services communicate through events rather than direct API calls.
Benefits:
- Loose Coupling: Services don’t need to know about each other
- Scalability: Easy to add new services that react to events
- Reliability: Events can be replayed if needed
- Asynchronous Processing: Non-blocking communication
Implementation:
// Event Publisher
@Component
public class OrderEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishOrderCreated(Order order) {
OrderCreatedEvent event = new OrderCreatedEvent(order);
eventPublisher.publishEvent(event);
}
}
// Event Listener
@Component
public class InventoryService {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
updateInventory(order.getItems());
}
}
// Event Class
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
}
Message Queues:
// RabbitMQ Configuration
@Configuration
public class RabbitConfig {
@Bean
public Queue orderQueue() {
return new Queue("order.queue", true);
}
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.exchange");
}
@Bean
public Binding binding(Queue orderQueue, TopicExchange orderExchange) {
return BindingBuilder.bind(orderQueue)
.to(orderExchange)
.with("order.created");
}
}
// Message Producer
@Component
public class OrderMessageProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrderCreated(Order order) {
rabbitTemplate.convertAndSend("order.exchange", "order.created", order);
}
}
// Message Consumer
@Component
public class OrderMessageConsumer {
@RabbitListener(queues = "order.queue")
public void handleOrderCreated(Order order) {
// Process order
processOrder(order);
}
}
3. CQRS (Command Query Responsibility Segregation)
What is CQRS?
Separating read and write operations into different models and data stores.
Benefits:
- Optimized Queries: Read models can be optimized for specific queries
- Scalability: Read and write operations can be scaled independently
- Performance: Different optimization strategies for reads and writes
- Flexibility: Can use different databases for reads and writes
Implementation:
// Command Side (Write Model)
@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal total;
private OrderStatus status;
public void confirm() {
this.status = OrderStatus.CONFIRMED;
// Publish event
domainEvents.add(new OrderConfirmedEvent(this));
}
}
// Query Side (Read Model)
@Entity
public class OrderSummary {
@Id
private Long id;
private Long userId;
private String userName;
private BigDecimal total;
private OrderStatus status;
private LocalDateTime createdAt;
}
// Command Handler
@Component
public class CreateOrderCommandHandler {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void handle(CreateOrderCommand command) {
Order order = new Order(command.getUserId(), command.getItems());
orderRepository.save(order);
}
}
// Query Handler
@Component
public class GetUserOrdersQueryHandler {
@Autowired
private OrderSummaryRepository orderSummaryRepository;
public List<OrderSummary> handle(GetUserOrdersQuery query) {
return orderSummaryRepository.findByUserIdOrderByCreatedAtDesc(query.getUserId());
}
}
4. Saga Pattern
What is Saga Pattern?
A pattern for managing distributed transactions across multiple services.
Benefits:
- Data Consistency: Ensures eventual consistency across services
- Fault Tolerance: Handles failures gracefully
- Scalability: Works well with microservices
- Flexibility: Can handle complex business workflows
Implementation:
// Saga Coordinator
@Component
public class OrderSaga {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Transactional
public void createOrder(CreateOrderRequest request) {
try {
// Step 1: Create Order
Order order = orderService.createOrder(request);
// Step 2: Reserve Inventory
inventoryService.reserveInventory(order.getItems());
// Step 3: Process Payment
paymentService.processPayment(order.getPayment());
// Step 4: Confirm Order
orderService.confirmOrder(order.getId());
} catch (Exception e) {
// Compensate for failures
compensate(order.getId());
throw e;
}
}
private void compensate(Long orderId) {
try {
orderService.cancelOrder(orderId);
inventoryService.releaseInventory(orderId);
paymentService.refundPayment(orderId);
} catch (Exception e) {
// Log compensation failure
log.error("Compensation failed for order: {}", orderId, e);
}
}
}
5. Circuit Breaker Pattern
What is Circuit Breaker?
A pattern that prevents cascading failures by monitoring for failures and stopping the flow of requests when a threshold is reached.
Benefits:
- Fault Tolerance: Prevents system-wide failures
- Fast Failure: Fails fast instead of timing out
- Recovery: Automatically recovers when the service is healthy
- Monitoring: Provides metrics on service health
Implementation:
// Circuit Breaker Configuration
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerFactory circuitBreakerFactory() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.ringBufferSizeInHalfOpenState(2)
.ringBufferSizeInClosedState(2)
.build();
return new DefaultCircuitBreakerFactory(config);
}
}
// Service with Circuit Breaker
@Service
public class UserServiceClient {
@Autowired
private CircuitBreakerFactory circuitBreakerFactory;
public User getUser(Long id) {
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("user-service");
return circuitBreaker.run(
() -> restTemplate.getForObject("/api/users/" + id, User.class),
throwable -> getDefaultUser(id)
);
}
private User getDefaultUser(Long id) {
return new User(id, "Default User", "default@example.com");
}
}
6. Bulkhead Pattern
What is Bulkhead Pattern?
Isolating different parts of a system so that a failure in one part doesn’t affect others.
Benefits:
- Fault Isolation: Failures are contained
- Resource Management: Better resource allocation
- Performance: Prevents resource exhaustion
- Reliability: Improves overall system reliability
Implementation:
// Thread Pool Configuration
@Configuration
public class ThreadPoolConfig {
@Bean("userServiceExecutor")
public Executor userServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("user-service-");
executor.initialize();
return executor;
}
@Bean("orderServiceExecutor")
public Executor orderServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("order-service-");
executor.initialize();
return executor;
}
}
// Service with Bulkhead
@Service
public class UserService {
@Autowired
@Qualifier("userServiceExecutor")
private Executor executor;
public CompletableFuture<User> getUserAsync(Long id) {
return CompletableFuture.supplyAsync(() -> {
// Simulate slow operation
Thread.sleep(1000);
return findUserById(id);
}, executor);
}
}
7. API Gateway Pattern
What is API Gateway?
A single entry point for all client requests that routes them to appropriate services.
Benefits:
- Centralized Control: Single point for authentication, logging, etc.
- Client Simplification: Clients don’t need to know about individual services
- Security: Centralized security policies
- Monitoring: Centralized monitoring and analytics
Implementation:
# Spring Cloud Gateway Configuration
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: user-service
fallbackUri: forward:/fallback/user
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: order-service
fallbackUri: forward:/fallback/order
8. Database Patterns
Read/Write Splitting
// Data Source Configuration
@Configuration
public class DatabaseConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource() {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBType.MASTER, masterDataSource());
targetDataSources.put(DBType.SLAVE, slaveDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
}
// Routing Data Source
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDBType();
}
}
// Context Holder
public class DBContextHolder {
private static final ThreadLocal<DBType> contextHolder = new ThreadLocal<>();
public static void setDBType(DBType dbType) {
contextHolder.set(dbType);
}
public static DBType getDBType() {
return contextHolder.get();
}
public static void clearDBType() {
contextHolder.remove();
}
}
Database Sharding
// Sharding Strategy
@Component
public class UserShardingStrategy {
public String getShardKey(Long userId) {
return "shard_" + (userId % 4);
}
public DataSource getDataSource(Long userId) {
String shardKey = getShardKey(userId);
return dataSourceMap.get(shardKey);
}
}
// Sharded Repository
@Repository
public class ShardedUserRepository {
@Autowired
private UserShardingStrategy shardingStrategy;
public User findById(Long userId) {
DataSource dataSource = shardingStrategy.getDataSource(userId);
// Use the appropriate data source
return executeOnDataSource(dataSource, () -> userRepository.findById(userId));
}
}
Key Architectural Principles
- Single Responsibility: Each service should have one clear purpose
- Loose Coupling: Services should be independent of each other
- High Cohesion: Related functionality should be grouped together
- Fault Tolerance: Design for failure and recovery
- Scalability: Design for horizontal scaling
- Observability: Include monitoring, logging, and tracing
- Security: Security should be built-in from the start
- Performance: Design for performance from the beginning
When to Use Each Pattern
- Microservices: Large, complex applications with multiple teams
- Event-Driven: Systems with many asynchronous operations
- CQRS: Applications with complex query requirements
- Saga: Distributed transactions across multiple services
- Circuit Breaker: External service dependencies
- Bulkhead: Resource-intensive operations
- API Gateway: Multiple services with common concerns
- Database Patterns: High data volume applications
Remember, these patterns are tools to solve specific problems. Choose the right pattern for your specific use case and requirements.