Spring MVC Exception Handling Interview Questions – Validation, @ControllerAdvice and Advanced Features

  • Last Updated: May 1, 2026
  • By: javahandson
  • Series
img

Spring MVC Exception Handling Interview Questions – Validation, @ControllerAdvice and Advanced Features

Spring MVC exception handling interview questions cover one of the most evaluated areas in Java backend interviews — and for good reason. Spring MVC is the web layer of the Spring Framework, built around a clean design in which every HTTP request flows through a single DispatcherServlet, which delegates to handler methods, applies validation, and returns responses via a structured, interceptable pipeline. For senior Java developers and Spring Boot engineers, deep knowledge of Spring MVC is not optional — it is the foundation on which production APIs are built.

This article covers 15 carefully crafted Spring MVC exception handling interview questions — from beginner to advanced — covering @Valid and @Validated input validation, BindingResult, custom validators, @ExceptionHandler, @ControllerAdvice, @RestControllerAdvice, @InitBinder, @ModelAttribute, Interceptors vs Filters, HandlerInterceptor, MockMvc testing, @WebMvcTest vs @SpringBootTest, Flash Attributes, global CORS configuration, and Spring MVC vs Spring WebFlux. Every answer includes the technical depth that interviewers at product companies expect.

Q1. How does input validation work in Spring MVC — @Valid vs @Validated?

Answer – Input validation in Spring MVC is based on the Bean Validation API (JSR-303/JSR-380), with Hibernate Validator as the default implementation. When a request reaches a controller method, Spring can automatically validate the incoming data — whether it arrives as a request body, path variable, request parameter, or form field — before the method body executes.

The @Valid annotation triggers standard Bean Validation on the annotated parameter. It is defined in javax.validation (or jakarta.validation in newer versions) and belongs to the Java standard. @Validated is a Spring-specific alternative that adds group-based validation on top of @Valid. Groups allow you to apply different constraint sets to the same class depending on the context — for example, applying stricter rules during creation than during an update.

// Request body DTO with Bean Validation constraints
@Data
public class CreateOrderRequest {
    @NotBlank(message = "Customer name is required")
    private String customerName;

    @Email(message = "Must be a valid email address")
    private String email;

    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    private int quantity;

    @NotNull(message = "Order date is required")
    @FutureOrPresent(message = "Order date must be today or in the future")
    private LocalDate orderDate;
}

// Controller — @Valid triggers Bean Validation on the request body
@RestController
@RequestMapping("/orders")
public class OrderController {

    // @Valid — validates using all constraints, no groups
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @RequestBody @Valid CreateOrderRequest request,
            BindingResult result) {
        if (result.hasErrors()) {
            // handle validation errors
        }
        return ResponseEntity.ok(orderService.create(request));
    }

    // @Validated with group — applies only constraints in UpdateGroup interface
    @PutMapping("/{id}")
    public ResponseEntity<OrderResponse> updateOrder(
            @PathVariable Long id,
            @RequestBody @Validated(UpdateGroup.class) UpdateOrderRequest request) {
        return ResponseEntity.ok(orderService.update(id, request));
    }
}

When @Valid or @Validated is present, Spring invokes the LocalValidatorFactoryBean — a Spring-managed wrapper around Hibernate Validator — before passing control to the method body. If validation fails and a BindingResult immediately follows the validated parameter in the method signature, errors are captured there. If no BindingResult is present, Spring throws MethodArgumentNotValidException, which produces a 400 Bad Request by default.

🎯 Interview Insight:  The key distinction interviewers probe: @Valid supports cascaded validation on nested objects using @Valid on the field inside the parent class. @Validated does NOT support cascading. Also, @Validated is required (not @Valid) when you want to trigger Bean Validation on method parameters in service-layer beans — for example, @Service class methods annotated with @Validated at the class level. Spring uses AOP to apply MethodValidationInterceptor for those cases.

Q2. What is BindingResult, and how do you use it to handle validation errors?

Answer – BindingResult is a Spring MVC interface that holds the result of a binding and validation operation on a model object. When Spring attempts to bind request data to a method parameter and validate it, any constraint violations or type mismatch errors are accumulated in the BindingResult — rather than thrown immediately as exceptions — provided that BindingResult is declared as the parameter immediately following the validated object.

BindingResult gives you fine-grained control over how validation errors are communicated to the client. Instead of relying on Spring’s default exception handling, you inspect the errors programmatically and build a custom response. This is the standard pattern in form-based web applications where you want to return the form with error messages inline.

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public ResponseEntity<?> registerUser(
            @RequestBody @Valid RegisterUserRequest request,
            BindingResult bindingResult) {   // MUST follow @Valid parameter directly

        if (bindingResult.hasErrors()) {
            // Collect all field errors into a map
            Map<String, String> errors = new LinkedHashMap<>();
            for (FieldError error : bindingResult.getFieldErrors()) {
                errors.put(error.getField(), error.getDefaultMessage());
            }
            // Return 400 with structured error body
            return ResponseEntity.badRequest().body(errors);
        }

        UserDto created = userService.register(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}

// Sample error response body:
// {
//   "email": "Must be a valid email address",
//   "password": "Password must be at least 8 characters"
// }

The BindingResult interface extends Errors. Key methods include hasErrors() to check if any errors exist, getFieldErrors() to retrieve per-field errors, getGlobalErrors() to retrieve object-level errors (those not tied to a specific field), getErrorCount() for the total count, and rejectValue(field, errorCode, message) to manually add errors. The Errors interface also provides reject() for adding global errors.

An important constraint: BindingResult must immediately follow the @Valid or @Validated parameter in the method signature. If there is any other parameter between them, Spring will not associate the BindingResult with the validated object and will throw MethodArgumentNotValidException even if the BindingResult is present in the method.

🎯 Interview Insight:  Interviewers sometimes ask: ‘What is the difference between FieldError and ObjectError?’ FieldError extends ObjectError and is tied to a specific field of the validated object. ObjectError represents a class-level constraint — for example, a cross-field validation where you check that startDate is before endDate. Custom class-level constraints added via @ScriptAssert or a custom class-level ConstraintValidator produce ObjectErrors, not FieldErrors. You retrieve them separately via getFieldErrors() vs getGlobalErrors().

Q3. How do you write a custom validator — Validator interface vs custom constraint annotation?

Answer – Spring MVC supports two approaches to custom validation: implementing the org.springframework.validation.Validator interface (a Spring-specific mechanism) and creating a custom Bean Validation constraint annotation backed by a ConstraintValidator implementation (the standard JSR-380 approach). In modern Spring MVC applications, the Bean Validation approach is strongly preferred because it integrates with @Valid, Hibernate Validator, and all standard validation tooling.

The Validator interface approach requires implementing two methods: supports(Class<?> clazz) returns true if the validator can validate instances of the given class, and validate(Object target, Errors errors) performs the validation logic and populates the Errors object. This approach is used in Spring MVC when you register the validator via @InitBinder or use it programmatically.

// Approach 1: Spring Validator interface
@Component
public class OrderRequestValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CreateOrderRequest.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        CreateOrderRequest request = (CreateOrderRequest) target;
        if (request.getQuantity() > 50 && request.getCustomerType() == CustomerType.RETAIL) {
            errors.rejectValue("quantity",
                "quantity.exceeds.retail.limit",
                "Retail customers cannot order more than 50 units");
        }
    }
}

// Register via @InitBinder
@Controller
public class OrderController {
    @Autowired private OrderRequestValidator orderRequestValidator;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(orderRequestValidator);
    }
}
// Approach 2: Custom Bean Validation annotation (preferred)

// Step 1: Define the annotation
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "Email address is already registered";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Step 2: Implement ConstraintValidator
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    @Autowired
    private UserRepository userRepository;

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null) return true; // @NotNull handles null
        return !userRepository.existsByEmail(email);
    }
}

// Step 3: Use on DTO field
public class RegisterUserRequest {
    @NotBlank
    @Email
    @UniqueEmail
    private String email;
}

The Bean Validation constraint annotation approach integrates seamlessly with @Valid and @Validated. Hibernate Validator instantiates the ConstraintValidator and, because Spring Boot configures the ValidatorFactory as a Spring bean, Spring can inject dependencies (@Autowired) into ConstraintValidator implementations. This makes it easy to write database-backed validators like the UniqueEmailValidator shown above.

🎯 Interview Insight:  A common interview follow-up: ‘Can you inject Spring beans into a ConstraintValidator?’ Yes — but only if the ValidatorFactory is configured as a Spring-managed bean, which Spring Boot does automatically. If you configure Hibernate Validator manually outside of Spring Boot, you need to call constraintValidatorFactory.use(SpringConstraintValidatorFactory.class) explicitly. Forgetting this is a frequent source of NullPointerExceptions in custom validators when autowired dependencies appear null at runtime.

Q4. What is @ExceptionHandler — scope and how does it work?

Answer – @ExceptionHandler is a Spring MVC annotation that marks a method as an exception handler for specific exception types. When an exception is thrown during request processing — from a controller method, a validator, or a message converter — Spring’s exception resolution infrastructure looks for an @ExceptionHandler method that matches the thrown exception type and delegates to it.

An @ExceptionHandler method declared inside a @Controller or @RestController class has local scope — it only handles exceptions thrown by handler methods in that same controller. This is the most basic usage. When you need exception handling that spans multiple controllers, you move the handlers to a @ControllerAdvice class, which is covered in Q5.

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    public ProductResponse getProduct(@PathVariable Long id) {
        return productService.findById(id)
            .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id));
    }

    @PostMapping
    public ProductResponse createProduct(@RequestBody @Valid CreateProductRequest req) {
        return productService.create(req);
    }

    // Handles ProductNotFoundException thrown anywhere in THIS controller
    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ProductNotFoundException ex) {
        ErrorResponse error = new ErrorResponse("NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    // Handles multiple exception types
    @ExceptionHandler({ IllegalArgumentException.class, IllegalStateException.class })
    public ResponseEntity<ErrorResponse> handleBadRequest(RuntimeException ex) {
        ErrorResponse error = new ErrorResponse("BAD_REQUEST", ex.getMessage());
        return ResponseEntity.badRequest().body(error);
    }

    // Can access HttpServletRequest for context
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse("INTERNAL_ERROR",
            "Request failed: " + request.getRequestURI());
        return ResponseEntity.internalServerError().body(error);
    }
}

@ExceptionHandler methods are extremely flexible in the method signatures they support. They can accept the exception itself, as well as HttpServletRequest, HttpServletResponse, WebRequest, Model, and other standard Spring MVC parameters. Their return type can be ResponseEntity<T>, a String view name, a model object, or void if they write directly to the response. The most common pattern in REST APIs is returning a ResponseEntity with a structured error body.

Spring matches exceptions to handlers using most-specific-type-first logic. If a NullPointerException is thrown and you have handlers for both NullPointerException and RuntimeException, the NullPointerException handler wins. If no handler is found in the current controller, Spring falls back to @ControllerAdvice beans and then to the default HandlerExceptionResolver chain.

🎯 Interview Insight:  A subtle but important point: @ExceptionHandler inside a controller takes priority over @ControllerAdvice beans for exceptions thrown by that controller. If both exist, the controller-local handler wins. This allows you to override a global handler for specific controllers without removing the global handler entirely.

Q5. What is @ControllerAdvice — how is its scope different from @ExceptionHandler on the controller?

Answer – @ControllerAdvice is a specialization of @Component that allows you to define cross-cutting concerns for multiple controllers in a single class. It acts as a global interceptor that applies to all controllers (or a scoped subset) in your application. The three things you can define inside a @ControllerAdvice are: @ExceptionHandler methods (global exception handling), @InitBinder methods (global data binder customization), and @ModelAttribute methods (global model attributes).

The fundamental scope difference between @ExceptionHandler on a controller and @ExceptionHandler on a @ControllerAdvice class: controller-level handlers are local — they apply only to the controller where they are declared. @ControllerAdvice handlers are global by default — they apply to all controllers in the application context.

@ControllerAdvice
public class GlobalExceptionHandler {

    // Handles MethodArgumentNotValidException — @Valid failures with no BindingResult
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors()
            .forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }

    // Handles ResourceNotFoundException — a custom application exception
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }

    // Catch-all for unhandled exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        // Log the full stack trace here
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
    }
}

@ControllerAdvice supports three attributes to narrow its scope. The basePackages (or value) attribute restricts the advice to controllers in specific packages. The assignableTypes attribute restricts it to controllers that are subtypes of specified classes. The annotations attribute restricts it to controllers annotated with specific annotations. These scoping options are covered in detail in the eBook Q2.

The resolution order when multiple @ControllerAdvice beans exist is determined by @Order or Ordered. Lower order value means higher priority. If two @ControllerAdvice beans both define a handler for the same exception type, the one with the higher priority (lower @Order value) wins.

🎯 Interview Insight:  A common interview question: ‘What happens if both a controller-local @ExceptionHandler and a @ControllerAdvice handler exist for the same exception?’ The controller-local handler always wins for exceptions thrown by that controller. Spring searches for an @ExceptionHandler in the current controller first, and only falls through to @ControllerAdvice if no matching handler is found locally. This allows per-controller customization without removing the global handler.

Q6. What is @RestControllerAdvice vs @ControllerAdvice?

Answer – @RestControllerAdvice is a composed annotation that combines @ControllerAdvice and @ResponseBody. The practical effect is identical to placing @ResponseBody on every @ExceptionHandler method inside a @ControllerAdvice class. For REST APIs in which all exception handler methods return serialized response bodies (JSON or XML) rather than view names, @RestControllerAdvice is the cleaner, more concise choice.

// These two are functionally equivalent:

// Option 1: @ControllerAdvice + @ResponseBody on every method
@ControllerAdvice
public class LegacyExceptionHandler {
    @ResponseBody
    @ExceptionHandler(NotFoundException.class)
    public ErrorResponse handleNotFound(NotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }
}

// Option 2: @RestControllerAdvice — @ResponseBody is implicit on all methods
@RestControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }
}

In practice, most modern Spring Boot REST APIs use @RestControllerAdvice exclusively for their global exception-handling class, since REST APIs always return serialized responses from exception handlers. @ControllerAdvice without @ResponseBody is used in Spring MVC applications that serve HTML views (Thymeleaf, JSP), where exception handlers might return view names instead of response bodies.

The distinction also matters for content negotiation. With @ResponseBody, Spring’s message converters serialize the return value based on the request’s Accept header. Without @ResponseBody, the return value is treated as a view name or model attribute, and Spring’s view resolution kicks in. For REST APIs, you always want @ResponseBody semantics.

🎯 Interview Insight:  Interviewers sometimes ask: ‘If I return a ResponseEntity from an @ExceptionHandler method, do I need @ResponseBody?’ No — ResponseEntity already carries the status, headers, and body, and Spring treats its presence as an implicit instruction to write directly to the response. @ResponseBody is only needed when you return a plain object (like ErrorResponse directly) and want Spring to serialize it. When in doubt, returning ResponseEntity<ErrorResponse> is the most explicit and safest approach.

Q7. What is @InitBinder — what can you customize with it?

Answer – @InitBinder is a Spring MVC annotation that marks a method to be called before each request handling in a controller (or globally in a @ControllerAdvice), with the purpose of initializing or customizing a WebDataBinder. The WebDataBinder is responsible for binding request parameters to method arguments and applying validation. @InitBinder methods give you a hook to configure binding behavior per request.

The most common use cases for @InitBinder are registering custom PropertyEditors or Converters for type conversion, registering custom Validators to supplement Bean Validation, setting allowed or disallowed fields to prevent mass-assignment vulnerabilities, and configuring how dates and numbers are parsed from request parameters.

@Controller
public class UserController {

    // @InitBinder applies to all handler methods in THIS controller
    // In @ControllerAdvice, it applies globally to all controllers
    @InitBinder
    public void initBinder(WebDataBinder binder) {

        // 1. Register a PropertyEditor for custom date format
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));

        // 2. Prevent binding to sensitive fields (mass assignment protection)
        binder.setDisallowedFields("id", "createdAt", "role", "passwordHash");

        // 3. Register a custom Spring Validator alongside Bean Validation
        binder.addValidators(new UserRequestValidator());

        // 4. Set field-level type conversion for numbers
        binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
    }

    // @InitBinder scoped to a specific model attribute name
    @InitBinder("orderRequest")
    public void initOrderBinder(WebDataBinder binder) {
        binder.addValidators(new OrderRequestValidator());
    }

    @PostMapping("/register")
    public String register(@ModelAttribute UserRequest request) {
        // binder has already applied disallowedFields and custom editors
        return "redirect:/users";
    }
}

When @InitBinder is placed in a @ControllerAdvice class, the binder customizations apply globally to all controllers. This is the correct pattern for registering a global date format or global security restrictions, like disallowed fields that should apply everywhere.

In modern Spring Boot applications with Jackson for JSON deserialization, @InitBinder is less commonly used for type conversion because Jackson handles date and number parsing from the request body independently. However, it remains important for form-based applications that use @ModelAttribute to bind request parameters and for mass-assignment protection via setDisallowedFields.

🎯 Interview Insight:  A security-relevant detail: setDisallowedFields() vs setAllowedFields(). setDisallowedFields is a blacklist — all fields except the listed ones are bindable. setAllowedFields is a whitelist — only the listed fields are bindable, everything else is rejected. For security-sensitive objects where you want to prevent mass assignment (binding to fields such as ‘role’, ‘admin’, ‘passwordHash’), using setAllowedFields with an explicit whitelist is the safer choice. Forgetting to use either one is how mass-assignment vulnerabilities enter Spring MVC applications.

Q8. What is the @ModelAttribute method-level vs parameter-level difference?

Answer – @ModelAttribute has two distinct uses in Spring MVC depending on where it is applied, and understanding both is essential. At the method level, @ModelAttribute marks a method whose return value is automatically added to the model before any @RequestMapping method is invoked. At the parameter level, @ModelAttribute binds request parameters (form fields, query params) to a method argument object.

// ============ METHOD-LEVEL @ModelAttribute ============
// Executes BEFORE every handler method in this controller
// Return value is added to the model under key 'categories'
@Controller
public class ProductController {

    @ModelAttribute("categories")
    public List<Category> populateCategories() {
        // Called before every handler in this controller
        return categoryService.findAll();
    }

    @ModelAttribute("currentUser")
    public User populateCurrentUser(Principal principal) {
        return userService.findByUsername(principal.getName());
    }

    @GetMapping("/products/new")
    public String newProductForm(Model model) {
        // model already contains 'categories' and 'currentUser' — no manual addition needed
        model.addAttribute("product", new ProductForm());
        return "product-form";
    }
}

// ============ PARAMETER-LEVEL @ModelAttribute ============
// Binds request parameters to the ProductForm object
// Also adds the object to the Model automatically
@PostMapping("/products")
public String createProduct(@ModelAttribute @Valid ProductForm form,
                            BindingResult result, RedirectAttributes attrs) {
    if (result.hasErrors()) {
        return "product-form";  // back to form with errors
    }
    productService.save(form);
    attrs.addFlashAttribute("success", "Product created successfully");
    return "redirect:/products";
}

At the parameter level, @ModelAttribute binds HTTP request parameters (query params and form fields) to the fields of the annotated object using the WebDataBinder. It first checks the model for an existing object with the same attribute name — if it finds one (placed there by a method-level @ModelAttribute or by the session), it binds to it; otherwise, it creates a new instance. This makes it easy to support pre-populated forms for edit use cases.

An important implicit behavior: in Spring MVC controller methods, if a parameter type is not a simple type (String, int, etc.) and no other annotation is present, Spring implicitly treats it as @ModelAttribute. You do not need to write @ModelAttribute explicitly — it is the default binding strategy for complex objects in Spring MVC controllers.

🎯 Interview Insight:  A frequently asked follow-up: ‘What is the difference between @ModelAttribute and @RequestBody?’ @ModelAttribute binds form parameters (application/x-www-form-urlencoded or multipart/form-data) by matching individual parameter names to object fields using WebDataBinder. @RequestBody deserializes the entire HTTP request body (JSON, XML) using HttpMessageConverter. For REST APIs, @RequestBody is standard. For HTML form submissions, @ModelAttribute is standard. Using @RequestBody on a form submission will fail because form data is sent as key-value pairs in the body, not as JSON.

Q9. What is the difference between Interceptors and Filters in Spring MVC?

Answer – Both Interceptors and Filters allow you to intercept HTTP requests and add cross-cutting logic — authentication, logging, performance measurement, request transformation — but they operate at different levels of the request processing stack and have different capabilities and lifecycle.

Filters are part of the Java Servlet specification. They sit in the servlet container layer, which means they execute before the DispatcherServlet even sees the request. A Filter can intercept any HTTP request, including those going to static resources, servlets other than DispatcherServlet, and actuator endpoints. Filters operate on raw HttpServletRequest and HttpServletResponse objects — they are unaware of Spring MVC handler methods, view names, or model objects.

Interceptors are a Spring MVC concept, implemented via the HandlerInterceptor interface. They execute within the DispatcherServlet, after Spring has resolved the handler (i.e., the controller method that will handle the request), but before the handler method itself runs. Interceptors have access to the handler object (HandlerMethod), which gives them information about the specific controller method, its annotations, and the handler’s class. This makes interceptors much more powerful for Spring MVC-specific use cases.

AspectFilter (Servlet)Interceptor (Spring MVC)
SpecificationJava EE / Jakarta EE (javax.servlet.Filter)Spring Framework (HandlerInterceptor)
Execution pointBefore DispatcherServletInside DispatcherServlet, after handler resolution
Access to handlerNo — no awareness of Spring controllersYes — receives HandlerMethod object
Request scopeAll requests (static, REST, actuator)Only requests handled by DispatcherServlet
Spring beans availableVia ApplicationContext lookup or @Autowired (Spring Boot)Full Spring context, @Autowired works naturally
Use forSecurity, CORS, compression, encodingAuth, logging with handler details, metrics per endpoint
🎯 Interview Insight:  A common interview question: ‘Which would you use for JWT authentication — a Filter or an Interceptor?’ The answer is a Filter, and this is what Spring Security does. Authentication must happen before DispatcherServlet dispatches to a controller, because you need to stop unauthenticated requests before any controller logic runs. An interceptor cannot fully prevent request processing the way a filter can, because by the time an interceptor sees the request, Spring MVC has already resolved the handler. Spring Security’s OncePerRequestFilter runs well before any interceptor.

Q10. How do you implement a HandlerInterceptor?

Answer – HandlerInterceptor is the Spring MVC interface for writing interceptors. It defines three methods corresponding to different stages of request processing: preHandle, postHandle, and afterCompletion. All three have default implementations that return true or do nothing, so you only need to override the ones relevant to your use case.

// HandlerInterceptor implementation
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);

    // Runs BEFORE the handler method. Return false to abort the request.
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        request.setAttribute("startTime", System.currentTimeMillis());
        if (handler instanceof HandlerMethod handlerMethod) {
            String controller = handlerMethod.getBeanType().getSimpleName();
            String method = handlerMethod.getMethod().getName();
            log.info("Request START: {} {} -> {}.{}",
                request.getMethod(), request.getRequestURI(), controller, method);
        }
        return true; // continue processing; return false to abort
    }

    // Runs AFTER the handler method, BEFORE the view is rendered.
    // NOT called if preHandle returned false or the handler threw an exception.
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {
        log.info("Request POST: {} status={}", request.getRequestURI(), response.getStatus());
    }

    // Runs AFTER view rendering is complete. Always called if preHandle returned true.
    // exception is non-null if the handler threw an unhandled exception.
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long elapsed = System.currentTimeMillis() - startTime;
        log.info("Request COMPLETE: {} {}ms", request.getRequestURI(), elapsed);
        if (ex != null) {
            log.error("Request failed with exception", ex);
        }
    }
}

// Register the interceptor via WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private RequestLoggingInterceptor requestLoggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLoggingInterceptor)
            .addPathPatterns("/api/**")         // apply to /api/* paths only
            .excludePathPatterns("/api/health"); // exclude health check endpoint
    }
}

The return value of preHandle is critical. Returning false aborts the entire request processing chain — the handler method is not called, postHandle is not called, and the interceptor itself is responsible for writing a response or setting an error status. This is how authentication interceptors block unauthorized requests.

The afterCompletion method is always called (for requests where preHandle returned true), even if the handler throws an exception. This makes it the right place for cleanup operations like releasing resources, closing database connections, or recording final metrics. The exception parameter will be non-null if the handler or postHandle threw an unhandled exception.

🎯 Interview Insight:  A nuanced detail interviewers ask about: ‘In what order are multiple interceptors invoked?’ For preHandle, interceptors execute in the order they are registered. For postHandle and afterCompletion, they execute in reverse order. So if you have interceptors A → B → C, preHandle runs A → B → C, but postHandle runs C → B → A, and afterCompletion runs C → B → A. This ensures that the last interceptor in the chain is the first to clean up — a symmetric nesting pattern. If interceptor A’s preHandle returns false, neither B nor C sees preHandle, and afterCompletion is called only for interceptors whose preHandle returned true (A’s afterCompletion is called, B and C’s are not).

Q11. What is MockMvc, and how do you test Spring MVC controllers?

Answer – MockMvc is a Spring Test framework component that allows you to test Spring MVC controllers without starting a real HTTP server. It simulates the full Spring MVC request processing pipeline — DispatcherServlet, handler mapping, argument resolvers, message converters, view resolution, and exception handling — in memory. This gives you the same behavior as a real HTTP request but with the speed of a unit test.

MockMvc is built on top of the Spring TestContext Framework. You set it up in a test class by either loading a full ApplicationContext with MockMvc configured (integration style) or by setting up a minimal standalone context for a single controller (unit test style). The builder API makes it easy to send requests and assert on responses.

// Integration style — loads the full Spring context
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void createOrder_withValidRequest_returns201() throws Exception {
        CreateOrderRequest request = new CreateOrderRequest("Alice", "alice@example.com", 5);

        mockMvc.perform(
            MockMvcRequestBuilders.post("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request))
        )
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.orderId").exists())
        .andExpect(jsonPath("$.customerName").value("Alice"))
        .andDo(print());  // prints request/response to console
    }

    @Test
    void createOrder_withInvalidEmail_returns400() throws Exception {
        CreateOrderRequest request = new CreateOrderRequest("Alice", "not-an-email", 5);

        mockMvc.perform(
            MockMvcRequestBuilders.post("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request))
        )
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.email").value("Must be a valid email address"));
    }
}

// Slice test style — @WebMvcTest loads only MVC layer
@WebMvcTest(OrderController.class)
class OrderControllerSliceTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;  // mock the service layer

    @Test
    void getOrder_whenNotFound_returns404() throws Exception {
        given(orderService.findById(99L)).willThrow(new OrderNotFoundException("Not found"));

        mockMvc.perform(get("/orders/99"))
            .andExpect(status().isNotFound());
    }
}

The fluent assertion API uses ResultMatchers from MockMvcResultMatchers. The status() matcher checks HTTP status codes. jsonPath() uses JsonPath expressions to assert on JSON response bodies. header() asserts on response headers. content() checks the content type or the response body string. You can chain as many assertions as needed and add andDo(print()) to dump the full request and response to the console for debugging.

🎯 Interview Insight:  A common interview question: ‘Can MockMvc test filters and interceptors?’ Yes — when you use @SpringBootTest + @AutoConfigureMockMvc or the full WebApplicationContext setup, MockMvc applies filters and interceptors registered in the application. In @WebMvcTest slice tests, filters registered as Spring beans are applied, but interceptors registered via WebMvcConfigurer are also loaded because the MVC configuration is part of the web layer slice. This makes MockMvc particularly valuable for testing the full MVC pipeline including security filters when Spring Security is on the classpath.

Q12. @WebMvcTest vs @SpringBootTest — when to use which for MVC testing?

Answer – @WebMvcTest and @SpringBootTest are both Spring Boot test annotations but they create very different application contexts and serve different testing goals. The choice between them determines how much of your application is loaded and how realistic the test environment is.

@WebMvcTest is a slice test annotation. It loads only the Spring MVC layer — controllers, @ControllerAdvice, @JsonComponent, Filter beans, WebMvcConfigurer, HandlerMethodArgumentResolver, and the MockMvc infrastructure. It does not load your @Service, @Repository, or @Component beans, nor does it configure a real database. This makes @WebMvcTest fast — context startup is measured in hundreds of milliseconds rather than seconds — and focused on testing the web layer in isolation.

@SpringBootTest loads the full ApplicationContext — every bean in your application. By default, it does not start an HTTP server, but you can configure it to use a real embedded server (RANDOM_PORT or DEFINED_PORT). When combined with @AutoConfigureMockMvc, it gives you MockMvc with the full application context, which is closer to a real integration test.

// @WebMvcTest — MVC layer only, fast, service layer must be mocked
@WebMvcTest(controllers = UserController.class)  // optionally restrict to specific controller
@Import(SecurityConfig.class)  // explicitly import security config if needed
class UserControllerWebMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean  // REQUIRED — @Service beans are NOT in @WebMvcTest context
    private UserService userService;

    @Test
    void getUser_returnsUserResponse() throws Exception {
        given(userService.findById(1L))
            .willReturn(new UserDto(1L, "Alice", "alice@example.com"));

        mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Alice"));
    }
}

// @SpringBootTest — full context, real integration, slower
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;  // real HTTP client

    @Test
    void createUser_persistsToDatabase() {
        CreateUserRequest req = new CreateUserRequest("Bob", "bob@example.com");
        ResponseEntity<UserDto> response = restTemplate.postForEntity("/users", req, UserDto.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getId()).isNotNull();
    }
}
🎯 Interview Insight:  A practical interview question: ‘My @WebMvcTest is failing because a controller depends on a @Service that depends on a @Repository. How do you fix it?’ The answer is @MockBean. Declare @MockBean for the service in your test class. @WebMvcTest does not load @Service beans, so without @MockBean the context fails to wire the controller. You then use Mockito’s given() to configure what the mock service returns. The test is fast and focused — you are testing controller behavior, not database behavior.

Q13. What are Flash Attributes in Spring MVC — when would you use them?

Answer – Flash Attributes are a mechanism in Spring MVC for passing data across a redirect without appending it to the URL as query parameters. They are stored temporarily in the HTTP session just before a redirect, survive the redirect, are made available as model attributes in the target request, and are then automatically removed from the session. This is how Spring MVC cleanly implements the Post/Redirect/Get pattern.

The problem flash attributes solve: after a successful form POST, the best practice is to redirect to a GET to prevent form resubmission on browser refresh. But how do you show the user a ‘success’ message on the GET page after the redirect? You cannot pass it in the URL (ugly and not bookmarkable), and the model is request-scoped (it dies with the POST request). Flash attributes bridge the gap — they survive exactly one redirect.

@Controller
@RequestMapping("/orders")
public class OrderController {

    // POST — process form, add flash attribute, redirect
    @PostMapping
    public String placeOrder(@ModelAttribute @Valid OrderForm form,
                             BindingResult result,
                             RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "order-form";  // stay on form, show errors
        }

        Order saved = orderService.save(form);

        // Flash attribute — survives ONE redirect, then removed from session
        redirectAttributes.addFlashAttribute("successMessage",
            "Order #" + saved.getId() + " placed successfully!");

        // addAttribute() adds to URL as query param — NOT flash
        redirectAttributes.addAttribute("orderId", saved.getId());

        return "redirect:/orders/" + saved.getId();
    }

    // GET — receives flash attribute in model automatically
    @GetMapping("/{id}")
    public String viewOrder(@PathVariable Long id,
                            Model model) {
        // 'successMessage' is available here if it was set in the redirect
        // It is NOT available on subsequent refreshes of this page
        Order order = orderService.findById(id);
        model.addAttribute("order", order);
        return "order-detail";
    }
}

Spring stores flash attributes in the HTTP session under a key that includes the redirect URL. On the next request to the redirect target, DispatcherServlet moves the flash attributes from the session into the model and then removes them. The mechanism is implemented by FlashMapManager (DefaultFlashMapManager by default), which saves and retrieves FlashMap objects from the session.

Flash attributes should not be used for large amounts of data because they temporarily occupy session memory. For a brief success or error message, they are the ideal solution. For complex objects, consider using the session directly or a dedicated session-scoped bean with @SessionAttributes.

🎯 Interview Insight:  A common follow-up: ‘What is the difference between RedirectAttributes.addFlashAttribute() and RedirectAttributes.addAttribute()?’ addFlashAttribute() stores data in the session for one redirect — it does not appear in the URL and is removed after being consumed. addAttribute() appends data as URL query parameters — it is visible in the URL, bookmarkable, and does not use the session. Use addFlashAttribute for transient messages and addAttribute for identifiers or filters that belong in the URL.

Q14.  How do you configure global CORS in Spring MVC?

Answer – Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts HTTP requests from one origin (domain, protocol, port) to another. Spring MVC supports CORS configuration at three levels: method-level via @CrossOrigin, controller-level via @CrossOrigin on the class, and globally via WebMvcConfigurer. For production applications, the global approach is standard because it centralizes CORS policy in one place.

// Global CORS configuration — applies to all endpoints
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com", "https://admin.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Authorization", "Content-Type", "Accept")
            .exposedHeaders("X-Total-Count", "X-Request-Id")
            .allowCredentials(true)
            .maxAge(3600);  // browser caches preflight for 1 hour

        // Different policy for public read-only endpoints
        registry.addMapping("/public/**")
            .allowedOriginPatterns("*")  // use pattern, not "*" with allowCredentials(true)
            .allowedMethods("GET")
            .allowCredentials(false);
    }
}

// Method-level @CrossOrigin — overrides or adds to global config for specific endpoints
@RestController
@RequestMapping("/api/partners")
@CrossOrigin(origins = "https://partner.external.com", maxAge = 1800)
public class PartnerController {

    @GetMapping
    @CrossOrigin(origins = "*")  // method-level overrides class-level for this endpoint
    public List<Partner> listPartners() {
        return partnerService.findAll();
    }
}

When Spring Security is on the classpath, CORS configuration must be coordinated with the security filter chain. The Spring MVC CORS filter and Spring Security’s CORS support both need to be configured; the security filter blocks CORS preflight OPTIONS requests before Spring MVC can handle them. The recommended approach with Spring Security is to configure CORS via CorsConfigurationSource inside the security filter chain configuration, using .cors(withDefaults()), which picks up the CorsConfigurationSource bean.

For Spring Boot applications without Spring Security, the global WebMvcConfigurer approach is sufficient. The allowCredentials(true) flag requires that allowedOrigins use specific origin strings — not wildcard ‘*’ — because sending credentials (cookies, authorization headers) to a wildcard origin is a security risk, and browsers will reject it. Use allowedOriginPatterns(“*”) only when allowCredentials is false.

🎯 Interview Insight:  A common interview topic: ‘What is a preflight request and how does Spring handle it?’ Browsers send an HTTP OPTIONS preflight request before any cross-origin non-simple request (e.g., a POST with JSON body or a PUT with an Authorization header) to ask the server if the actual request is permitted. Spring MVC automatically handles OPTIONS preflight requests for paths covered by CORS mappings — you do not need to write an OPTIONS handler. Spring responds with the appropriate Access-Control-Allow-* headers. The maxAge setting controls how long browsers cache this preflight response, reducing redundant OPTIONS round-trips.

Q15. What is Spring MVC vs Spring WebFlux — when to choose what?

Answer – Spring MVC and Spring WebFlux are the two web frameworks in the Spring ecosystem, and they represent fundamentally different programming models and runtime architectures. Spring MVC is the original synchronous, blocking, servlet-based framework. Spring WebFlux, introduced in Spring 5, is a reactive, non-blocking framework that natively supports asynchronous execution.

Spring MVC uses a thread-per-request model. Each incoming HTTP request is assigned a thread from a thread pool (typically from Tomcat or Jetty). That thread handles the entire request synchronously — it blocks while waiting for database queries, external API calls, or file I/O. This model is simple to understand and debug, but it does not scale efficiently under high concurrent load because blocking threads consume memory and increase context-switching costs.

Spring WebFlux uses an event-loop model (backed by Netty by default or other reactive runtimes). A small number of threads handle a large number of concurrent requests non-blocking. When a WebFlux handler needs to perform a database query or an external API call, it registers a callback and releases the thread to handle other requests. Results arrive via reactive streams — Mono (0 or 1 item) and Flux (0 to N items) from Project Reactor.

// Spring MVC — synchronous, blocking
@RestController
@RequestMapping("/orders")
public class OrderController {

    @GetMapping("/{id}")
    public OrderResponse getOrder(@PathVariable Long id) {
        // Thread blocks here waiting for DB result
        return orderRepository.findById(id)
            .map(OrderResponse::from)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }
}

// Spring WebFlux — reactive, non-blocking
@RestController
@RequestMapping("/orders")
public class ReactiveOrderController {

    private final ReactiveOrderRepository orderRepository;

    @GetMapping("/{id}")
    public Mono<OrderResponse> getOrder(@PathVariable Long id) {
        // No thread blocking — event loop handles async completion
        return orderRepository.findById(id)
            .map(OrderResponse::from)
            .switchIfEmpty(Mono.error(new OrderNotFoundException(id)));
    }

    @GetMapping
    public Flux<OrderResponse> getAllOrders() {
        return orderRepository.findAll().map(OrderResponse::from);
    }
}

One important point: you cannot mix blocking code into a reactive WebFlux application without careful isolation. Calling a blocking JDBC driver or Thread.sleep() inside a WebFlux handler ties up the event loop thread, destroying the concurrency benefits. WebFlux requires reactive all the way down — reactive database drivers (R2DBC), reactive HTTP client (WebClient instead of RestTemplate), and reactive message brokers.

🎯 Interview Insight:  The most pragmatic interview answer on choosing between them: Spring MVC is the right choice for the vast majority of enterprise applications — CRUD services, monoliths, team environments without reactive expertise. Spring WebFlux shines for services that need to handle tens of thousands of concurrent connections with low memory overhead, services that aggregate many external API calls in parallel, or applications where streaming (Server-Sent Events, WebSocket) is a first-class requirement. Do not choose WebFlux because it is ‘modern’ — choose it because you have a concurrency problem that Spring MVC cannot solve efficiently.

Conclusion

Spring MVC’s exception handling, validation, and advanced configuration features form the backbone of every production-grade Java web API. This article has covered the full surface area that interviewers probe — from @Valid and BindingResult for input validation, to @ControllerAdvice and @RestControllerAdvice for centralized Spring MVC exception handling, to Interceptors, MockMvc testing, CORS, and the architectural decision between Spring MVC and WebFlux.

Key takeaways from this article:

@Valid triggers Bean Validation on request bodies and form objects. @Validated adds group-based validation and is required for service-layer method validation. BindingResult must immediately follow the validated parameter—if it is missing, Spring automatically throws a MethodArgumentNotValidException.

Custom validators can be written either as Spring’s Validator interface (registered via @InitBinder) or as Bean Validation constraint annotations backed by ConstraintValidator. The constraint annotation approach is preferred because it integrates with @Valid and Hibernate Validator and supports Spring bean injection.

@ControllerAdvice provides global exception handling, @InitBinder customization, and @ModelAttribute population across multiple controllers. @RestControllerAdvice is the REST-specific variant that adds implicit @ResponseBody to all handler methods.

HandlerInterceptors execute inside DispatcherServlet after handler resolution, with access to the handler method and its annotations. Filters execute before DispatcherServlet and are the right place for security and encoding concerns.

@WebMvcTest is the right choice for fast, focused MVC-layer tests. @SpringBootTest is for full integration tests. MockMvc’s fluent assertion API covers status, JSON path, headers, and content type.

Global CORS configuration via WebMvcConfigurer centralizes the CORS policy. When Spring Security is present, CORS must also be configured within the security filter chain. Choose Spring WebFlux over Spring MVC only when you have a genuine concurrency problem that the thread-per-request model cannot solve.

Leave a Comment