Skip to content

Spring MVC

Spring MVC implements the Model-View-Controller pattern for web applications. The DispatcherServlet is the front controller that routes requests to @Controller/@RestController methods. Key annotations: @RequestMapping, @GetMapping, @PostMapping, @PathVariable, @RequestBody, @ResponseBody. @RestController = @Controller + @ResponseBody. Supports exception handling via @ControllerAdvice.


Request Flow

Flow: Client → DispatcherServlet → HandlerMapping (finds controller) → HandlerAdapter (invokes method) → Controller → response. For @RestController, the return object is serialized to JSON via Jackson's HttpMessageConverter. For @Controller, a ViewResolver resolves the view name to a template.

Deep Dive: Processing Steps
Client → DispatcherServlet → HandlerMapping → Controller
Client ← ViewResolver ← View ← Model      (or @ResponseBody → JSON)
  1. Request arrives at DispatcherServlet
  2. HandlerMapping finds the controller method
  3. HandlerAdapter invokes it
  4. Controller returns model + view name (or response body)
  5. ViewResolver resolves view (if applicable)
  6. View renders response → sent to client

With @RestController: steps 5-6 skipped, Jackson serializes to JSON.


Controller Annotations

@RestController = @Controller + @ResponseBody. Use @GetMapping, @PostMapping, @PutMapping, @DeleteMapping for HTTP methods. Extract data with: @PathVariable (URL path), @RequestParam (query string), @RequestBody (JSON body), @RequestHeader (headers). Use @Valid to trigger bean validation.

Deep Dive: Full CRUD Example
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public List<User> getAllUsers() { ... }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) { ... }

    @GetMapping("/search")
    public List<User> search(
        @RequestParam String name,
        @RequestParam(defaultValue = "0") int page) { ... }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@Valid @RequestBody CreateUserRequest request) { ... }

    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id,
                           @Valid @RequestBody UpdateUserRequest request) { ... }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) { ... }
}
Annotation Purpose
@PathVariable Extract from URL path /users/{id}
@RequestParam Extract query param ?name=John
@RequestBody Deserialize JSON body
@RequestHeader Extract HTTP header
@ResponseStatus Set HTTP status code
@Valid Trigger bean validation

Request/Response Body & Jackson

Jackson (included with spring-boot-starter-web) handles JSON ↔ Java serialization. Use record DTOs for request/response objects. Validate input with @NotBlank, @Email, @Min annotations. Customize Jackson with @JsonFormat, @JsonInclude, or a custom ObjectMapper bean.

Deep Dive: DTOs and Jackson Config
// Request DTO with validation
public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

// Response DTO with formatting
public record UserResponse(
    Long id, String name, String email,
    @JsonFormat(pattern = "yyyy-MM-dd") LocalDate createdAt
) {}
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

Exception Handling with @ControllerAdvice

@RestControllerAdvice provides global exception handling across all controllers. Use @ExceptionHandler(ExceptionType.class) methods to handle specific exceptions and return consistent error responses. This centralizes error logic instead of repeating try-catch in every controller.

Deep Dive: Global Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage()));
        return new ErrorResponse("VALIDATION_FAILED", errors.toString());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        log.error("Unexpected error", ex);
        return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
    }
}

public record ErrorResponse(String code, String message) {}

Filters vs Interceptors

Filters operate at the Servlet level (before/after DispatcherServlet) — used for logging, CORS, security. Interceptors operate at the Spring MVC level (within DispatcherServlet) — used for auth, localization, have full access to Spring context. Filters see all requests; interceptors only see DispatcherServlet requests.

Deep Dive: Examples

Filter:

@Component
public class LoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        log.info("Request: {} {}", request.getMethod(), request.getRequestURI());
        chain.doFilter(request, response);
        log.info("Response: {}", response.getStatus());
    }
}

Interceptor:

@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res,
            Object handler) {
        return true;  // Return false to block request
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/auth/**");
    }
}

Feature Filter Interceptor
Level Servlet Spring MVC
Spring context Limited Full access
Applied to All requests DispatcherServlet only

ResponseEntity

ResponseEntity<T> gives full control over the HTTP response — status code, headers, and body. Use it when you need to return different status codes conditionally (e.g., 200 vs 404), set custom headers, or return 201 Created with a Location header.

Deep Dive: Usage Patterns
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
    return userService.findById(id)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest req) {
    User user = userService.create(req);
    URI location = URI.create("/api/users/" + user.getId());
    return ResponseEntity.created(location).body(user);
}

Common Interview Questions

Common Interview Questions
  • What is the DispatcherServlet?
  • Explain the request processing flow in Spring MVC.
  • What is the difference between @Controller and @RestController?
  • What is the difference between @PathVariable and @RequestParam?
  • How does @RequestBody work?
  • How do you handle exceptions globally in Spring MVC?
  • What is @ControllerAdvice?
  • What is the difference between a Filter and an Interceptor?
  • What is ResponseEntity and when would you use it?