Intermediate Reading time: ~8 min

Spring MVC

DispatcherServlet, @RestController, request mapping, validation, exception handling

Spring MVC

Spring MVC is a synchronous, request-response web framework built on the Servlet API, organized around the DispatcherServlet.


1. Definition

Spring MVC (Model-View-Controller) is the web module of the Spring Framework, built on the jakarta.servlet.http.HttpServlet API. The central element is the DispatcherServlet, which implements the Front Controller pattern: every HTTP request arrives at a single entry point, and the framework routes it to the appropriate handler method based on the URL, HTTP method, and other conditions.

Spring MVC is synchronous and blocking by default: each request occupies a servlet thread for the entire duration of processing. Spring Boot automatically configures the embedded Tomcat/Jetty/Undertow server and the DispatcherServlet.

HTTP Request → DispatcherServlet → HandlerMapping → HandlerAdapter
    → Controller → Service → Repository → Response

2. Core Concepts

DispatcherServlet architecture

The DispatcherServlet coordinates the following components:

Component Role
HandlerMapping URL → Controller method resolution
HandlerAdapter Invoking the controller method
HttpMessageConverter Request/response body conversion (JSON, XML)
ViewResolver View name → template resolution (Thymeleaf, JSP)
HandlerExceptionResolver Exception handling
LocaleResolver Locale resolution
MultipartResolver File upload handling

Controller annotations

@Controller          // View-returning controller
@RestController      // = @Controller + @ResponseBody (JSON/XML)
@RequestMapping      // Class or method level URL mapping
@GetMapping          // HTTP GET shortcut
@PostMapping         // HTTP POST shortcut
@PutMapping          // HTTP PUT shortcut
@DeleteMapping       // HTTP DELETE shortcut
@PatchMapping        // HTTP PATCH shortcut

Parameter binding

@PathVariable        // URL path variable: /users/{id}
@RequestParam        // Query parameter: ?name=John
@RequestBody         // HTTP body → Java object (deserialization)
@RequestHeader       // HTTP header values
@CookieValue         // Cookie values
@ModelAttribute      // Form data → Java object
@RequestPart         // Multipart file

Request processing flow

  1. HTTP request arrives at the DispatcherServlet
  2. HandlerMapping finds the matching controller method
  3. HandlerInterceptor.preHandle() executes
  4. HandlerAdapter invokes the controller method
  5. HttpMessageConverter deserializes the request body
  6. Controller executes business logic
  7. HttpMessageConverter serializes the response body
  8. HandlerInterceptor.postHandle() executes
  9. Response is sent back to the client

3. Practical Usage

REST API controller

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

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserDto> findAll(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(PageRequest.of(page, size));
    }

    @GetMapping("/{id}")
    public UserDto findById(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    @PutMapping("/{id}")
    public UserDto update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

Bean Validation integration

public record CreateUserRequest(
        @NotBlank(message = "Name is required")
        @Size(min = 2, max = 100)
        String name,

        @Email(message = "Invalid email format")
        @NotBlank
        String email,

        @Min(18) @Max(150)
        int age
) {}

@Valid on the controller parameter activates validation. If validation fails, a MethodArgumentNotValidException is thrown.

Global exception handling

@RestControllerAdvice
public class GlobalExceptionHandler {

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

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .toList();
        return new ErrorResponse("VALIDATION_FAILED", errors.toString());
    }

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

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

4. Code Examples

Content negotiation

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

    @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE,
                            MediaType.APPLICATION_XML_VALUE})
    public List<Product> findAll() {
        return productService.findAll();
    }
}

Based on the Accept header, Spring selects the appropriate HttpMessageConverter.

Custom HttpMessageConverter

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(
            List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(
                new ObjectMapper()
                    .registerModule(new JavaTimeModule())
                    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        ));
    }
}

Async controller (DeferredResult)

@GetMapping("/async/{id}")
public DeferredResult<UserDto> findByIdAsync(@PathVariable Long id) {
    DeferredResult<UserDto> result = new DeferredResult<>(5000L);
    executorService.submit(() -> {
        try {
            UserDto user = userService.findById(id);
            result.setResult(user);
        } catch (Exception e) {
            result.setErrorResult(e);
        }
    });
    return result;
}

DeferredResult releases the servlet thread while processing continues in the background.

ResponseEntity fine-tuning

@GetMapping("/{id}")
public ResponseEntity<UserDto> findById(@PathVariable Long id) {
    return userService.findOptional(id)
            .map(user -> ResponseEntity.ok()
                    .header("X-Custom", "value")
                    .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
                    .body(user))
            .orElse(ResponseEntity.notFound().build());
}

5. Trade-offs

Spring MVC vs WebFlux

Aspect Spring MVC WebFlux
I/O model Blocking (thread-per-request) Non-blocking (event loop)
Threads ~200 Tomcat threads (default) Few event loop threads
Scaling Vertical (more threads) Horizontal (fewer resources)
Debugging Simple stack traces Reactive chains harder
Ecosystem Full (JPA, JDBC, etc.) Limited (R2DBC, WebClient)
Learning curve Low High

When to use Spring MVC

  • CRUD applications with traditional databases (JPA/JDBC)
  • Simple REST APIs with moderate concurrent requests
  • Team unfamiliar with reactive programming
  • Using synchronous third-party libraries

When NOT to use Spring MVC

  • 10,000+ simultaneous connections (WebSocket, SSE)
  • Microservice gateway with high throughput
  • Streaming processing (Kafka, event sourcing)

6. Common Mistakes

❌ Business logic in controllers

// BAD: controller contains business logic
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) {
    if (userRepo.existsByEmail(req.email())) {
        throw new DuplicateException("Email exists");
    }
    User user = new User(req.name(), req.email());
    user = userRepo.save(user);
    emailService.sendWelcome(user);
    return UserDto.from(user);
}

// GOOD: delegate to service layer
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
    return userService.create(req);
}

❌ Missing @Valid annotation

// BAD: validation does not execute
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) { ... }

// GOOD: @Valid activates Bean Validation
@PostMapping
public UserDto create(@Valid @RequestBody CreateUserRequest req) { ... }

❌ API without exception handler

Validation and business errors leak as stack traces to the client. Always use @RestControllerAdvice!

❌ Forgetting @ResponseStatus

@PostMapping returns 200 OK by default. For resource creation, @ResponseStatus(HttpStatus.CREATED) is correct.

❌ Unhandled PathVariable type mismatch

// /api/users/abc → NumberFormatException → 500
// Handle with @ExceptionHandler or use regex:
@GetMapping("/{id:\\d+}")
public UserDto findById(@PathVariable Long id) { ... }

7. Deep Dive

HandlerInterceptor vs Servlet Filter

Aspect HandlerInterceptor Servlet Filter
Registration WebMvcConfigurer @Component / FilterRegistrationBean
Spring context Accessible (Spring bean) Only if Spring-managed
Execution point Handler (controller) level Servlet level (runs earlier)
preHandle Yes Before doFilter()
postHandle Yes (before response) No separate phase
afterCompletion Yes (after response) No separate phase

HttpMessageConverter internals

Spring selects the converter based on Accept and Content-Type headers:

  1. Request: Content-Type: application/json → MappingJackson2HttpMessageConverter.read()
  2. Response: Accept: application/json → MappingJackson2HttpMessageConverter.write()
  3. If Jackson is on the classpath, automatic registration occurs
  4. For XML, jackson-dataformat-xml dependency is required

Controller advice

@ControllerAdvice(basePackages = "com.example.api")
public class ApiAdvice {
    // Applies only to controllers in the api package

    @ModelAttribute
    public void addCommonAttributes(Model model) {
        model.addAttribute("appVersion", "2.0");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(LocalDate.class,
                new PropertyEditorSupport() {
                    @Override
                    public void setAsText(String text) {
                        setValue(LocalDate.parse(text));
                    }
                });
    }
}

Custom Argument Resolver

public class CurrentUserArgumentResolver
        implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter param) {
        return param.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter param,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) {
        return SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
    }
}

8. Interview Questions

  1. What is the role of DispatcherServlet? Front Controller pattern: single entry point, uses HandlerMapping to find the controller method, HandlerAdapter to invoke it.

  2. What is the difference between @Controller and @RestController? @RestController = @Controller + @ResponseBody. @Controller returns a View name (Thymeleaf), @RestController returns a JSON/XML body.

  3. How does validation work in Spring MVC? @Valid / @Validated activates Bean Validation (Hibernate Validator). On failure, MethodArgumentNotValidException is thrown. @RestControllerAdvice handles it globally.

  4. What is @RestControllerAdvice? A global exception handler + model attribute + init binder. Combination of @ControllerAdvice + @ResponseBody.

  5. How do you customize the HTTP status code? @ResponseStatus annotation, ResponseEntity<T> return type, or HttpServletResponse.setStatus().

  6. What is the difference between HandlerInterceptor and Servlet Filter? Filter: servlet level, runs without Spring context. Interceptor: Spring-managed, has preHandle/postHandle/afterCompletion lifecycle.

  7. How do you handle file uploads? @RequestPart MultipartFile file. A MultipartResolver bean is needed (Spring Boot auto-configures it). spring.servlet.multipart.max-file-size sets the limit.


9. Glossary

Term Meaning
DispatcherServlet Front Controller, single entry point for all HTTP requests
HandlerMapping URL → controller method resolution
HandlerAdapter Responsible for invoking the controller method
HttpMessageConverter Java object ↔ HTTP body conversion
@RestController @Controller + @ResponseBody, for JSON/XML APIs
@RequestMapping URL and HTTP method mapping annotation
@Valid Activates Bean Validation on controller parameter
@RestControllerAdvice Global exception handler + model attribute
ResponseEntity HTTP status + headers + body fine-tuning
HandlerInterceptor Spring-level pre/post processing
Content negotiation Accept/Content-Type based format selection
DeferredResult Async response (releases servlet thread)

10. Cheatsheet

REQUEST LIFECYCLE:
  Client → Filter → DispatcherServlet → Interceptor.preHandle()
    → HandlerAdapter → Controller → Service → Repository
    → Interceptor.postHandle() → Response

ANNOTATIONS:
  @RestController          JSON/XML API controller
  @RequestMapping("/api")  Base URL prefix
  @GetMapping("/{id}")     GET + path variable
  @PostMapping             POST endpoint
  @Valid @RequestBody      Body validation
  @PathVariable            URL segment
  @RequestParam            Query parameter
  @ResponseStatus(201)     HTTP status override

EXCEPTION HANDLING:
  @RestControllerAdvice    Global handler
  @ExceptionHandler(X)     Handle specific exception
  ResponseEntity<Error>    Custom response

CONFIGURATION:
  WebMvcConfigurer         Interceptor, converter, CORS
  server.port=8080         Server port
  spring.jackson.*         JSON serialization settings

TESTING:
  @WebMvcTest              Controller layer test (slice)
  MockMvc                  HTTP request simulation
  @MockBean                Service mock

🎼 Games

10 questions