import HeaderLink from './HeaderLink.astro';

Scaling Patterns and Architectural Strategies

scaling architecture patterns microservices system-design

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

  1. Single Responsibility: Each service should have one clear purpose
  2. Loose Coupling: Services should be independent of each other
  3. High Cohesion: Related functionality should be grouped together
  4. Fault Tolerance: Design for failure and recovery
  5. Scalability: Design for horizontal scaling
  6. Observability: Include monitoring, logging, and tracing
  7. Security: Security should be built-in from the start
  8. 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.