Skip to main content

Command Palette

Search for a command to run...

ReLearn : System Design and Architecture - Spring Boot Implementation

Updated
8 min read

In the world of software engineering, system design is the art and science of building large-scale distributed systems that are scalable, reliable, and maintainable. This guide will walk you through the essential components and considerations of system design with a focus on real-world implementations using Java Spring and microservices.

Why System Design Matters

Modern applications need to handle:

  • Millions of concurrent users

  • Petabytes of data

  • Sub-second response times

  • 99.99% uptime requirements

  • Global distribution

Understanding system design principles is crucial for building systems that can meet these demanding requirements while remaining maintainable and cost-effective.

Core System Components

1. Load Balancer Implementation

Load balancers are crucial for distributing traffic across multiple service instances. Here’s how to implement it with Spring Cloud:

@Configuration
public class LoadBalancerConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Service
public class UserService {
    private final RestTemplate restTemplate;

    public UserService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public UserProfile getUserProfile(Long userId) {
        // Load balanced request to user-profile-service
        return restTemplate.getForObject(
            "http://user-profile-service/api/profiles/{id}",
            UserProfile.class,
            userId
        );
    }
}

2. API Gateway Pattern

Implementing API Gateway using Spring Cloud Gateway:

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user_route", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .rewritePath("/api/(?<segment>.*)", "/${segment}")
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                    .retry(config -> config.setRetries(3))
                )
                .uri("lb://user-service")
            )
            .route("order_route", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .circuitBreaker(config -> config
                        .setName("orderCircuitBreaker")
                        .setFallbackUri("forward:/fallback/orders"))
                )
                .uri("lb://order-service")
            )
            .build();
    }
}j

3. Service Registry

Implementing service discovery with Eureka:

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}

# application.yml for Eureka Server
server:
  port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0
    enableSelfPreservation: false

# Client Service Configuration
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

4. Configuration Management

Implementing centralized configuration with Spring Cloud Config:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

# application.yml for Config Server
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/config-repo
          searchPaths: '{application}'
          default-label: main
          clone-on-start: true
          force-pull: true

# Client Configuration
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "app")
public class ApplicationConfig {
    private String apiKey;
    private int maxConnections;
    private Duration timeout;

    // Getters and setters
}

5. Message Queue Integration

Implementing async communication with Spring Cloud Stream:

@Configuration
public class MessageQueueConfig {
    @Bean
    public Function<Message<OrderEvent>, Message<OrderProcessedEvent>> processOrder() {
        return message -> {
            OrderEvent order = message.getPayload();
            // Process order
            return MessageBuilder
                .withPayload(new OrderProcessedEvent(order.getId()))
                .build();
        };
    }
}
# application.yml
spring:
  cloud:
    stream:
      bindings:
        processOrder-in-0:
          destination: orders
          group: order-processing-group
        processOrder-out-0:
          destination: processed-orders
      kafka:
        binder:
          brokers: localhost:9092

Data Management and Storage

1. Database Sharding Implementation

@Configuration
public class ShardingConfiguration {
    @Bean
    public DataSource shardingDataSource() {
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put("ds0", createDataSource("jdbc:mysql://shard1/db"));
        dataSourceMap.put("ds1", createDataSource("jdbc:mysql://shard2/db"));

        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTableRuleConfigs().add(getUserTableRuleConfiguration());

        return ShardingDataSourceFactory.createDataSource(
            dataSourceMap,
            shardingRuleConfig,
            new Properties()
        );
    }

    private TableRuleConfiguration getUserTableRuleConfiguration() {
        TableRuleConfiguration result = new TableRuleConfiguration("user", 
            "ds${0..1}.user${0..1}");
        result.setTableShardingStrategyConfig(
            new StandardShardingStrategyConfiguration("id", 
                new UserIdShardingAlgorithm())
        );
        return result;
    }
}

2. Caching Strategy Implementation

@Configuration
@EnableCaching
public class CacheConfiguration {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(60))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(Map.of(
                "users", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofMinutes(10)),
                "products", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofHours(1))
            ))
            .build();
    }
}

@Service
public class ProductService {
    private final ProductRepository repository;

    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public Product getProduct(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @CacheEvict(value = "products", key = "#id")
    public void updateProduct(Long id, ProductUpdateRequest request) {
        Product product = repository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
        // Update product
        repository.save(product);
    }

    @Caching(evict = {
        @CacheEvict(value = "products", key = "#id"),
        @CacheEvict(value = "product-recommendations", allEntries = true)
    })
    public void deleteProduct(Long id) {
        repository.deleteById(id);
    }
}

3. Event Sourcing Pattern

@Entity
@Table(name = "events")
public class Event {
    @Id
    @GeneratedValue
    private Long id;

    private String aggregateId;
    private String type;
    private String data;
    private LocalDateTime timestamp;
    private Long version;
}

@Service
public class OrderEventSourcingService {
    private final EventStore eventStore;
    private final ObjectMapper objectMapper;

    public Order reconstructOrderState(String orderId) {
        List<Event> events = eventStore.getEvents(orderId);
        Order order = new Order(orderId);

        for (Event event : events) {
            switch (event.getType()) {
                case "ORDER_CREATED":
                    OrderCreatedEvent created = parse(event, OrderCreatedEvent.class);
                    order.apply(created);
                    break;
                case "ORDER_UPDATED":
                    OrderUpdatedEvent updated = parse(event, OrderUpdatedEvent.class);
                    order.apply(updated);
                    break;
                // Handle other events
            }
        }

        return order;
    }

    public void saveEvent(String orderId, Object event) {
        Event dbEvent = new Event();
        dbEvent.setAggregateId(orderId);
        dbEvent.setType(event.getClass().getSimpleName());
        dbEvent.setData(objectMapper.writeValueAsString(event));
        dbEvent.setTimestamp(LocalDateTime.now());

        eventStore.save(dbEvent);
    }
}

4. CQRS Implementation

// Command Side
@Service
public class OrderCommandService {
    private final OrderRepository repository;
    private final EventPublisher eventPublisher;

    @Transactional
    public void createOrder(CreateOrderCommand command) {
        Order order = new Order(command.getCustomerId(), command.getItems());
        repository.save(order);

        eventPublisher.publish(new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems()
        ));
    }
}

// Query Side
@Service
public class OrderQueryService {
    private final OrderProjection orderProjection;

    public OrderDTO getOrder(String orderId) {
        return orderProjection.getOrder(orderId);
    }

    public List<OrderDTO> getCustomerOrders(String customerId) {
        return orderProjection.getOrdersByCustomer(customerId);
    }
}

// Event Handler
@Service
public class OrderEventHandler {
    private final OrderProjection orderProjection;

    @EventListener
    public void on(OrderCreatedEvent event) {
        orderProjection.handle(event);
    }

    @EventListener
    public void on(OrderUpdatedEvent event) {
        orderProjection.handle(event);
    }
}

Scalability and Performance

1. Horizontal Scaling Implementation

@Configuration
public class ScalingConfiguration {
    @Bean
    public HazelcastInstance hazelcastInstance() {
        Config config = new Config();
        config.setInstanceName("hazelcast-instance")
              .addMapConfig(
                  new MapConfig()
                      .setName("configuration-map")
                      .setEvictionConfig(
                          new EvictionConfig()
                              .setEvictionPolicy(EvictionPolicy.LRU)
                              .setMaxSizePolicy(MaxSizePolicy.PER_NODE)
                              .setSize(10000)
                      )
              );

        return Hazelcast.newHazelcastInstance(config);
    }
}

@Service
public class DistributedCacheService {
    private final HazelcastInstance hazelcastInstance;

    public void putValue(String key, Object value) {
        IMap<String, Object> map = hazelcastInstance.getMap("distributed-map");
        map.put(key, value);
    }

    public Optional<Object> getValue(String key) {
        IMap<String, Object> map = hazelcastInstance.getMap("distributed-map");
        return Optional.ofNullable(map.get(key));
    }
}

2. Asynchronous Processing

@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("AsyncThread-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

@Service
public class OrderProcessingService {
    private final NotificationService notificationService;
    private final InventoryService inventoryService;

    @Async
    public CompletableFuture<OrderResult> processOrder(Order order) {
        return CompletableFuture.supplyAsync(() -> {
            // Process order asynchronously
            OrderResult result = new OrderResult();

            // Parallel processing using CompletableFuture
            CompletableFuture<Void> notification = 
                CompletableFuture.runAsync(() -> 
                    notificationService.notifyCustomer(order.getCustomerId()));

            CompletableFuture<InventoryResult> inventory = 
                CompletableFuture.supplyAsync(() -> 
                    inventoryService.updateInventory(order.getItems()));

            // Wait for all async operations to complete
            CompletableFuture.allOf(notification, inventory).join();

            result.setInventoryResult(inventory.get());
            return result;
        });
    }
}

3. Rate Limiting

@Configuration
public class RateLimitingConfig {
    @Bean
    public RateLimiter rateLimiter() {
        return RateLimiter.create(100.0); // 100 requests per second
    }
}

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final RateLimiter rateLimiter;
    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
        if (!rateLimiter.tryAcquire()) {
            return ResponseEntity
                .status(HttpStatus.TOO_MANY_REQUESTS)
                .body("Too many requests - please try again later");
        }

        return ResponseEntity.ok(orderService.createOrder(request));
    }
}

// Bucket4j implementation for more sophisticated rate limiting
@Service
public class RateLimitingService {
    private final LoadingCache<String, Bucket> cache;

    public RateLimitingService() {
        Bandwidth limit = Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1)));

        this.cache = Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofHours(1))
            .build(key -> Bucket.builder()
                .addLimit(limit)
                .build());
    }

    public boolean tryConsume(String key) {
        return cache.get(key).tryConsume(1);
    }
}

4. Performance Monitoring

@Configuration
public class MetricsConfig {
    @Bean
    MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }
}

@Service
@Timed("service.orders")
public class OrderMetricsService {
    private final MeterRegistry meterRegistry;
    private final Counter orderCounter;
    private final Timer processTimer;

    public OrderMetricsService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.orderCounter = meterRegistry.counter("orders.created");
        this.processTimer = meterRegistry.timer("orders.processing.time");
    }

    public void recordOrderCreation() {
        orderCounter.increment();
    }

    public void recordProcessingTime(long milliseconds) {
        processTimer.record(milliseconds, TimeUnit.MILLISECONDS);
    }

    @Scheduled(fixedRate = 60000)
    public void reportMetrics() {
        Gauge.builder("orders.backlog", orderQueue, Queue::size)
            .tag("status", "pending")
            .register(meterRegistry);
    }
}

Security and Resilience

1. OAuth2 Security Implementation

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2ResourceServer()
                .jwt()
            .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("https://auth-server/.well-known/jwks.json")
            .build();
    }
}

@RestController
@RequestMapping("/api")
public class SecuredController {
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/stats")
    public AdminStats getAdminStats() {
        // Only accessible by admin users
        return adminService.getStats();
    }

    @PreAuthorize("#userId == authentication.principal.id")
    @GetMapping("/users/{userId}")
    public UserProfile getUserProfile(@PathVariable String userId) {
        // Only accessible by the user themselves
        return userService.getProfile(userId);
    }
}

2. Circuit Breaker Pattern

@Configuration
public class ResilienceConfig {
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofSeconds(60))
            .permittedNumberOfCallsInHalfOpenState(10)
            .slidingWindowSize(100)
            .build();

        return CircuitBreakerRegistry.of(config);
    }
}

@Service
public class PaymentService {
    private final CircuitBreaker circuitBreaker;
    private final PaymentGateway paymentGateway;

    public PaymentService(CircuitBreakerRegistry registry) {
        this.circuitBreaker = registry.circuitBreaker("payment-service");
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return circuitBreaker.executeSupplier(() -> {
            try {
                return paymentGateway.processPayment(request);
            } catch (Exception e) {
                throw new PaymentProcessingException("Payment failed", e);
            }
        });
    }

    private PaymentResult fallback(PaymentRequest request, Exception ex) {
        // Fallback logic when circuit is open
        return PaymentResult.builder()
            .status(PaymentStatus.PENDING)
            .message("Service temporarily unavailable")
            .build();
    }
}

3. Retry Pattern

@Configuration
public class RetryConfig {
    @Bean
    public RetryRegistry retryRegistry() {
        RetryConfig config = RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.ofSeconds(2))
            .retryExceptions(TimeoutException.class, IOException.class)
            .ignoreExceptions(IllegalArgumentException.class)
            .build();

        return RetryRegistry.of(config);
    }
}

@Service
public class ExternalServiceClient {
    private final Retry retry;
    private final WebClient webClient;

    public ExternalServiceClient(RetryRegistry registry) {
        this.retry = registry.retry("external-service");
    }

    public ExternalData fetchData(String id) {
        return retry.executeSupplier(() -> 
            webClient.get()
                .uri("/api/data/" + id)
                .retrieve()
                .bodyToMono(ExternalData.class)
                .block()
        );
    }
}

4. Distributed Tracing

@Configuration
public class TracingConfig {
    @Bean
    public Tracer jaegerTracer() {
        return new Configuration("my-service")
            .withSampler(new Configuration.SamplerConfiguration()
                .withType(ConstSampler.TYPE)
                .withParam(1))
            .withReporter(new Configuration.ReporterConfiguration()
                .withLogSpans(true)
                .withSender(new Configuration.SenderConfiguration()
                    .withEndpoint("http://jaeger-collector:14268/api/traces")))
            .getTracer();
    }
}

@Service
public class OrderProcessingService {
    private final Tracer tracer;

    public OrderResult processOrder(Order order) {
        Span span = tracer.buildSpan("process-order")
            .withTag("orderId", order.getId())
            .start();

        try (Scope scope = tracer.scopeManager().activate(span)) {
            // Process order
            span.setTag("status", "processing");
            OrderResult result = orderProcessor.process(order);
            span.setTag("status", "completed");
            return result;
        } catch (Exception e) {
            span.setTag("error", true);
            span.log(Map.of(
                "event", "error",
                "message", e.getMessage()
            ));
            throw e;
        } finally {
            span.finish();
        }
    }
}

The guide covers building scalable systems using Spring Boot and architectural principles