ReLearn : System Design and Architecture - Spring Boot Implementation
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