← Spring Boot & Microservices Interview Guide
Chapter 58 min read

Microservices Design Patterns Interview Questions

Circuit breaker with Resilience4j, the Saga pattern for distributed transactions, and the architecture questions that separate a senior answer from a mid-level one.

Q14. What is the Circuit Breaker pattern, and how do you implement it with Resilience4j?

A circuit breaker exists to stop one failing downstream service from cascading into an outage of everything that calls it.

The three states

  • Closed — normal operation, calls pass through
  • Open — the downstream service is failing, calls are rejected immediately without even attempting the network call
  • Half-Open — after a cooldown period, a limited number of calls are allowed through to test whether the downstream service has recovered

Implementation

@Service
public class UserService {

    @CircuitBreaker(name = "userService", fallbackMethod = "fallback")
    public User getUser(Long id) {
        return restTemplate.getForObject(
            "http://user-service/users/" + id, User.class);
    }

    public User fallback(Long id, Exception ex) {
        return new User(id, "Default User");
    }
}
resilience4j.circuitbreaker:
  instances:
    userService:
      sliding-window-size: 10
      failure-rate-threshold: 50
      wait-duration-in-open-state: 10000
      permitted-number-of-calls-in-half-open-state: 3

Use it anywhere you're calling an external service, another microservice, or a database that can genuinely fail independently of your own service's health — the goal is to fail fast and return a sane fallback instead of a thread pool slowly filling up with requests waiting on a service that's already down.

Q15. Explain the Saga pattern for distributed transactions.

A traditional ACID transaction doesn't exist across service boundaries — there's no single database to roll back if step 3 of a 5-step process fails. The Saga pattern solves this with a sequence of local transactions, each publishing an event that triggers the next step, with an explicit compensating transaction defined for every step in case something downstream fails.

Two ways to coordinate a saga:

  • Choreography — each service listens for events and decides what to do next on its own, with no central coordinator. Simple for a short saga, hard to reason about once you have more than three or four steps, because the overall flow lives implicitly across every service's event handlers instead of in one place.
  • Orchestration — a central saga orchestrator explicitly calls each step and issues compensating actions on failure. More code upfront, but the entire business process is visible in one place, which matters a lot once you're debugging why an order got stuck halfway through checkout at 2am.

The interview-killer follow-up: compensating transactions aren't the same as a database rollback — they're a new forward-moving action that semantically undoes the effect (refund a payment, release a reserved inventory count, cancel a shipment), and they need to be idempotent themselves, because the saga's own failure-retry logic can trigger them more than once.

Q16. How do you implement Async processing in Spring Boot?

1. Enable Async Support

@SpringBootApplication
@EnableAsync
public class Application {}

2. Async Methods

@Service
public class EmailService {
    
    @Async
    public CompletableFuture<String> sendEmail(String to) {
        // Long-running email sending
        Thread.sleep(5000);
        return CompletableFuture.completedFuture("Sent to " + to);
    }
    
    @Async
    public void processAsync() {
        // Fire and forget
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}

3. Custom Thread Pool

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            System.err.println("Exception in async method: " + method.getName());
            ex.printStackTrace();
        };
    }
}

4. Using CompletableFuture

@RestController
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/users/{id}")
    public CompletableFuture<User> getUser(@PathVariable Long id) {
        return userService.findUserAsync(id)
            .thenApply(user -> {
                // Transform result
                return user;
            });
    }
}

Async work needs a bounded, observable executor, explicit exception handling, and a clear policy for whether callers wait for the result; blocking those threads defeats the point and can exhaust the pool under load. The production follow-up is how queue capacity and rejection behavior protect the service when downstream work slows down.

Q17. Explain Transaction Management in Spring Boot (@Transactional)

1. Basic Usage

@Service
public class UserService {
    
    @Transactional
    public void updateUser(User user) {
        userRepository.save(user);
        auditRepository.log("User updated");
        // Both operations in same transaction
    }
}

2. Transaction Propagation

@Transactional(propagation = Propagation.REQUIRED)  // Default, join existing
@Transactional(propagation = Propagation.REQUIRES_NEW)  // Always new
@Transactional(propagation = Propagation.MANDATORY)  // Must have existing
@Transactional(propagation = Propagation.NESTED)  // Nested transaction
@Transactional(propagation = Propagation.NEVER)  // No transaction allowed

3. Isolation Levels

@Transactional(isolation = Isolation.READ_COMMITTED)  // Default
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)

4. Rollback Rules

@Transactional(
    rollbackFor = Exception.class,  // Rollback on any exception
    noRollbackFor = BusinessException.class  // Don't rollback
)
public void process() {
    // Business logic
}

5. Read-Only Optimization

@Transactional(readOnly = true)
public List<User> getAllUsers() {
    return userRepository.findAll();
    // Optimizes for read operations
}

@Transactional is proxy-based, so it applies to public calls that pass through the Spring proxy; self-invocation bypasses it, and checked exceptions do not roll back by default. The subtle point is to define transaction boundaries around a complete unit of work and verify rollback behavior with an integration test.

Q18. How do you secure Spring Boot REST APIs?

1. Spring Security Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer()
                .jwt();
        
        return http.build();
    }
}

3. JWT Authentication

@Service
public class JwtService {
    
    private final String SECRET_KEY = "your-secret-key";
    
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            .compact();
    }
    
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
}

4. Method-Level Security

@EnableGlobalMethodSecurity(prePostEnabled = true)

@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
    
    @PreAuthorize("#id == authentication.principal.id")
    public User getUser(Long id) {
        return userRepository.findById(id);
    }
}

5. OAuth2 Configuration

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth-server.com

The security answer is incomplete without key rotation, token validation, authorization at the right resource boundary, and deliberate CSRF handling for the client type. Interviewers are usually testing whether you understand that authentication proves identity while authorization decides what that identity may do.

Q19. Explain Spring Boot Testing strategies and annotations

1. Unit Testing

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testGetUser() {
        User mockUser = new User(1L, "John");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
        
        User result = userService.getUser(1L);
        
        assertEquals("John", result.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

2. Integration Testing

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void testGetUser() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"));
    }
}

3. Web Layer Testing

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testGetUser() throws Exception {
        when(userService.getUser(1L))
            .thenReturn(new User(1L, "John"));
        
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.name").value("John"));
    }
}

4. Repository Testing

@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testFindByName() {
        User user = new User("John");
        entityManager.persist(user);
        entityManager.flush();
        
        User found = userRepository.findByName("John");
        
        assertEquals(user.getName(), found.getName());
    }
}

5. Test Profiles

@SpringBootTest
@ActiveProfiles("test")
class ServiceTest {
    // Uses application-test.properties
}

Choose the narrowest test slice that proves the behavior in question, then reserve full-context tests for wiring and integration concerns. The trade-off worth stating is that fast unit tests give better feedback, while slice and integration tests catch configuration errors that mocks cannot see.

Q20. How do you handle Exception Handling in Spring Boot REST APIs?

1. @ControllerAdvice (Global Exception Handler)

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            ValidationException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal server error",
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

2. Custom Exception Classes

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long id) {
        super("User not found with id: " + id);
    }
}

3. Validation Error Handling

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
        MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}

4. Error Response DTO

public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;
    private List<String> errors;
    
    // Constructor, getters, setters
}

A global handler should produce a stable, non-sensitive error contract and log enough context for diagnosis without returning stack traces to clients. The senior detail is to distinguish expected client errors from unexpected server failures in both status codes and observability.

Q29. Explain Database-per-Service pattern and its challenges

Concept:

Each microservice has its own database, ensuring loose coupling.

User Service → User DB (PostgreSQL)
Order Service → Order DB (MongoDB)
Payment Service → Payment DB (MySQL)

Benefits:

  • Service independence
  • Technology diversity
  • Scalability per service
  • Fault isolation

Challenges:

ChallengeSolution
Distributed transactionsSaga pattern
Data consistencyEventual consistency
Joins across servicesAPI composition, CQRS
Data duplicationEvent-driven sync

Implementation Pattern:

// Each service has its own repository
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;  // Own DB
    
    // No direct access to other service's DB
}

Database-per-service preserves ownership and independent scaling, but it trades simple joins and ACID coordination for APIs, events, and eventual consistency. It is justified when service boundaries are real; splitting databases without clear ownership only moves coupling into harder-to-debug integration code.

Q30. What is CQRS pattern? When to use it?

CQRS: Command Query Responsibility Segregation

Concept:

Commands (Write):  → Write Database (Normalized)
    - CreateUser
    - UpdateUser
    - DeleteUser

Queries (Read):    → Read Database (Denormalized)
    - GetUser
    - SearchUsers
    - GetUserOrders

Event Store → Sync from Write to Read DB

Implementation:

// Command Side
@Service
public class UserCommandService {
    @Autowired
    private UserWriteRepository writeRepo;
    
    public void createUser(CreateUserCommand cmd) {
        User user = new User(cmd.getName(), cmd.getEmail());
        writeRepo.save(user);
        eventPublisher.publish(new UserCreatedEvent(user));
    }
}

// Query Side
@Service
public class UserQueryService {
    @Autowired
    private UserReadRepository readRepo;
    
    public UserDTO getUser(Long id) {
        return readRepo.findById(id);  // Optimized for reads
    }
}

When to Use:

  • Different scalability needs for read/write
  • Complex domain logic
  • Need for optimized read models
  • Event sourcing

CQRS makes sense when reads and writes have materially different scaling, consistency, or modeling needs; for a simple CRUD service, it usually adds synchronization and operational burden without enough payoff. The interview follow-up is how the read model catches up, is rebuilt, and handles duplicate or out-of-order events.

Krishna Kumar Yadav

Krishna Kumar Yadav

Senior Software Engineer building distributed systems at scale. 9+ years across fintech, airline tech, and startups.