Skip to content

Simplified Learning Blog

Learning made easy

  • Home
  • Modern Java
  • Architecture & Design
    • Cloud Native
    • System Design
  • AI Engineering
  • Resources
  • About Us
  • Toggle search form

Practical System Design Examples using Spring Boot, Queues, and Caches (2026 Guide)

Posted on January 10, 2026January 14, 2026 By Govind No Comments on Practical System Design Examples using Spring Boot, Queues, and Caches (2026 Guide)

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%.

Table of Contents

Toggle
  • From CRUD to Architecture | Spring Boot System Design Examples
  • Pattern 1: The Asynchronous Decoupler (The “Email Service” Problem)
    • Implementation (Spring Boot 3.4 + RabbitMQ)
  • Pattern 2: The Distributed Throttler (The “API Limit” Problem)
    • Implementation (Spring Boot + Redis)
  • Pattern 3: The Look-Aside Cache (The “Hot Data” Problem)
    • Implementation (Spring Cache Abstraction)
  • FAQ – Senior Design Edition
  • Conclusion: Design for Failure

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.

  1. Check Redis.
  2. If found (Hit) -> Return.
  3. 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.

Govind

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/

Related

Architecture & Design Tags:Spring Boot System Design Examples

Post navigation

Previous Post: Spring MVC vs. Spring Boot in 2026: The Senior Architect’s Definitive Guide
Next Post: How to Handle Errors in Spring Boot REST APIs (2026 Guide): The Global Exception Handling Pattern

More Related Articles

Event-Driven Architecture in 2026: Kafka vs. Pulsar vs. Redpanda Architecture & Design
Software Architecture Performance Architecture & Design
Performance Principles of Software Architecture Architecture & Design
System Performance Objective Architecture & Design

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • Event-Driven Architecture in 2026: Kafka vs. Pulsar vs. Redpanda
  • Building a RAG Pipeline with Spring AI and pgvector (No Python Required)
  • Technical Debt vs. Feature Velocity: A Framework for Tech Leads (2026)
  • Testing Asynchronous Flows with Awaitility: The End of Flaky Tests
  • Migrating from Java 8/11 to Java 25: The Refactoring Checklist (2026 Edition)

Recent Comments

  1. Govind on Performance Principles of Software Architecture
  2. Gajanan Pise on Performance Principles of Software Architecture
Simplified Learning

Demystifying complex enterprise architecture for senior engineers. Practical guides on Java, Spring Boot, and Cloud Native systems.

Explore

  • Home
  • About Us
  • Author Profile: Govind
  • Contact Us

Legal

  • Privacy Policy
  • Terms and Conditions
  • Disclaimer
© 2026 Simplified Learning Blog. All rights reserved.
We use cookies to improve your experience and personalize ads. By continuing, you agree to our Privacy Policy and use of cookies.

Powered by PressBook Green WordPress theme