From CRUD to Architecture | Spring Boot System Design Examples
As a Senior Developer, your value isn’t just writing @RestController. It’s knowing how to prevent that controller from crashing when traffic spikes by 500%.
In 2026, “System Design” isn’t just for whiteboard interviews at FAANG. It’s about how you glue Spring Boot together with infrastructure components like RabbitMQ and Redis to build resilient applications.
This guide breaks down three practical, production-grade patterns you can implement today. We move beyond “Hello World” to solve real scalability problems.
Pattern 1: The Asynchronous Decoupler (The “Email Service” Problem)
The Problem: A user registers (POST /users). You need to send a Welcome Email.
- Naive Approach: You call
emailService.send()inside the controller. - The Failure Mode: The SMTP server hangs for 5 seconds. The user’s registration request hangs for 5 seconds. If 1000 users register, your Tomcat thread pool is exhausted, and the site goes down.
The Solution: Decouple the “Core Domain” (User Registration) from the “Side Effect” (Email) using a Message Queue (RabbitMQ or Kafka).
Implementation (Spring Boot 3.4 + RabbitMQ)
1. The Producer (Controller) Instead of sending the email, we publish an event. The API responds instantly (201 Created).
@RestController
@RequestMapping("/api/users")
public class UserRegistrationController {
private final StreamBridge streamBridge; // Spring Cloud Stream
@PostMapping
public ResponseEntity<String> register(@RequestBody UserDto user) {
// 1. Save User to DB (Core Logic)
userService.save(user);
// 2. Publish Event (Fire & Forget)
UserRegisteredEvent event = new UserRegisteredEvent(user.getEmail(), user.getName());
streamBridge.send("email-out-0", event);
return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully");
}
}
2. The Consumer (Worker Service) This runs in a separate process or thread. If the Email server is down, the message stays in the queue. No data is lost, and the user is never blocked.
@Component
public class EmailWorker {
@Bean
public Consumer<UserRegisteredEvent> sendWelcomeEmail() {
return event -> {
try {
// Simulate slow 3rd party call
emailProvider.send(event.email(), "Welcome!");
} catch (Exception e) {
// Message automatically requeued or sent to DLQ (Dead Letter Queue)
throw new AmqpRejectAndDontRequeueException(e);
}
};
}
}
Pattern 2: The Distributed Throttler (The “API Limit” Problem)
The Problem: You have a public API. One customer writes a script that spams your endpoint 10,000 times a second. Your database CPU hits 100%, taking down the site for everyone.
The Solution: Implement a Token Bucket Rate Limiter using Redis. We check the limit before doing any work.
Implementation (Spring Boot + Redis)
We create a custom Aspect (@RateLimited) to protect specific endpoints.
1. The Aspect (The Gatekeeper)
@Aspect
@Component
public class RateLimitAspect {
private final StringRedisTemplate redisTemplate;
@Around("@annotation(rateLimited)")
public Object checkLimit(ProceedingJoinPoint joinPoint, RateLimited rateLimited) throws Throwable {
String apiKey = getApiKey(); // Extract from header
String key = "rate_limit:" + apiKey;
// Atomic Increment in Redis
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// Set expiration window (e.g., 1 minute)
redisTemplate.expire(key, 60, TimeUnit.SECONDS);
}
if (count > rateLimited.maxRequests()) {
throw new TooManyRequestsException("Limit exceeded. Try again later.");
}
return joinPoint.proceed();
}
}
2. The Usage Now, protecting an expensive endpoint is trivial.
@RateLimited(maxRequests = 100) // 100 req/min
@GetMapping("/expensive-report")
public Report generateReport() {
return reportService.crunchBigData();
}
Pattern 3: The Look-Aside Cache (The “Hot Data” Problem)
The Problem: Your e-commerce site has a “Product Details” page. During a flash sale, 50,000 people view the same iPhone page.
- Naive Approach: Every request hits the SQL database.
- The Failure Mode: The DB creates 50k connections. The connection pool exhausts. The site crashes.
The Solution: Look-Aside Caching.
- Check Redis.
- If found (Hit) -> Return.
- If missing (Miss) -> Query DB -> Save to Redis -> Return.
Implementation (Spring Cache Abstraction)
Spring Boot makes this declarative via @Cacheable.
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProduct(String id) {
// This line only executes if Redis does NOT have the data
System.out.println("Fetching from Database...");
return productRepository.findById(id).orElse(null);
}
// Critical: Cache Eviction
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepository.save(product);
// Next 'get' will re-fetch from DB automatically
}
}
Architectural Warning (Cache Stampede): In 2026, the standard @Cacheable isn’t enough for high concurrency. If the cache expires and 10,000 requests hit simultaneously, they will all go to the DB (The “Thundering Herd” problem).
- Fix: Enable “Sync” locking in Spring:
@Cacheable(value = "products", sync = true)This ensures only one thread queries the DB; the other 9,999 wait for that one thread to populate the cache.
FAQ – Senior Design Edition
Q: Why use RabbitMQ instead of Java’s CompletableFuture? A: CompletableFuture is in-memory. If your server restarts, the pending tasks are lost forever. RabbitMQ persists messages to disk. If the consumer crashes, the message remains in the queue and is retried later.
Q: Should I use Redis or Caffeine for caching? A:
- Caffeine: Local (In-Memory). Fast (nanoseconds). Good for single-instance monoliths.
- Redis: Distributed. Slower (milliseconds). Required for Microservices where multiple instances need to see the same cache state.
Q: How do I handle “Dead Letters” in RabbitMQ? A: Always configure a DLQ (Dead Letter Queue). If a message fails processing 3 times (e.g., due to a bug), move it to a generic error-queue. Do not let it block the main queue indefinitely.
Conclusion: Design for Failure
Practical system design is about assuming things will break.
- The Email server will time out -> Use Queues.
- The Database will get overloaded -> Use Caches.
- The Users will abuse the API -> Use Rate Limiters.
By applying these three patterns in Spring Boot, you transform fragile code into a resilient system capable of handling the unexpected.

For over 15 years, I have worked as a hands-on Java Architect and Senior Engineer, specializing in building and scaling high-performance, enterprise-level applications. My career has been focused primarily within the FinTech, Telecommunications, or E-commerce sector, where I’ve led teams in designing systems that handle millions of transactions per day.
Checkout my profile here : AUTHOR https://simplifiedlearningblog.com/author/