Skip to content

7 Essential Spring MVC Concepts Every Spring Boot Developer Must Master

Problem

I see this pattern over and over with Spring Boot developers. They start with @SpringBootApplication, add some @RestController classes, and everything works. Until it doesn’t.

When their API returns a 404 for no apparent reason, they’re stuck. When a request takes 10 seconds and they don’t know why, they guess. When they need to customize exception handling, they paste code from Stack Overflow without understanding it.

The core issue: Spring Boot’s auto-configuration is so good that developers never learn what’s happening under the hood.

A Reddit user put it perfectly: “If you start with Boot, just make sure to eventually learn the Spring MVC core concepts (like the DispatcherServlet, the Request Lifecycle, and Annotations), because when something breaks or performance drops, you’ll need to know how the underlying ‘engine’ actually works to fix it.”

This creates a black box problem. You can build applications, but you can’t debug them effectively.

Why Spring MVC Matters for Boot Developers

Spring Boot is not a new framework. It’s Spring MVC with auto-configuration. When you understand this, everything clicks.

┌──────────────────────────────────────────────────────────┐
│ Spring Boot Application │
├──────────────────────────────────────────────────────────┤
│ │
│ @SpringBootApplication │
│ │ │
│ ├── @SpringBootConfiguration → @Configuration │
│ ├── @EnableAutoConfiguration → Magic happens │
│ └── @ComponentScan → Find your beans │
│ │
│ All of this builds on SPRING MVC │
│ │
└──────────────────────────────────────────────────────────┘

I think every Spring Boot developer should master these 7 concepts.

1. DispatcherServlet: The Front Controller

The DispatcherServlet is the heart of Spring MVC. It receives every HTTP request and coordinates the entire processing pipeline.

Request Flow
HTTP Request
┌─────────────────────┐
│ DispatcherServlet │ ← Front Controller Pattern
│ (Single Entry Point)│
└──────────┬──────────┘
├──→ Which controller handles this?
├──→ Call the controller method
├──→ Process the return value
└──→ Send response back

Spring Boot auto-configures DispatcherServlet, but understanding its role helps when:

  • Debugging why your endpoint isn’t being called
  • Customizing request processing behavior
  • Adding servlet filters for authentication or logging

Here’s how Boot configures it:

What Boot Does Automatically
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(true);
return dispatcherServlet;
}

When I need to customize it, I can override this bean.

2. Request Lifecycle: Following the Path

Understanding the request lifecycle is essential for debugging. Here’s what happens when a request hits your application:

Spring MVC Request Lifecycle
┌─────────────────────────────────────────────────────────────┐
│ 1. HTTP Request arrives at DispatcherServlet │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. HandlerMapping determines which controller method │
│ Example: @GetMapping("/users/{id}") → UserController │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. HandlerAdapter invokes the controller method │
│ - Binds parameters (@PathVariable, @RequestBody) │
│ - Calls the method │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. Controller returns a view name or data │
│ - Traditional MVC: "userProfile" (logical view name) │
│ - REST API: User object (to be serialized) │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. ViewResolver or MessageConverter processes the return │
│ - MVC: ViewResolver → Thymeleaf/JSP template │
│ - REST: MessageConverter → JSON via Jackson │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6. Response sent back to client │
└─────────────────────────────────────────────────────────────┘

When something goes wrong, I ask myself:

ProblemLikely StageDebug Strategy
404 Not FoundHandlerMappingCheck @RequestMapping path
Parameter nullHandlerAdapterCheck @PathVariable/@RequestParam
Wrong JSON formatMessageConverterCheck Jackson annotations
View not foundViewResolverCheck template location

3. Controller Annotations: The Building Blocks

Controller annotations define how HTTP requests map to Java methods. I see developers confuse these constantly.

@Controller vs @RestController

UserController.java
@Controller
public class UserController {
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id, Model model) {
model.addAttribute("user", userService.findById(id));
return "user-view"; // Returns view name
}
}
UserRestController.java
@RestController // = @Controller + @ResponseBody
public class UserRestController {
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Serialized to JSON
}
}

The key difference: @RestController adds @ResponseBody to every method, meaning return values are serialized directly (not resolved as view names).

Request Mapping Annotations

Mapping Examples
// Class-level base path
@RestController
@RequestMapping("/api/users")
public class UserRestController {
// GET /api/users
@GetMapping
public List<User> getAllUsers() { ... }
// GET /api/users/{id}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { ... }
// POST /api/users
@PostMapping
public User createUser(@RequestBody @Valid CreateUserRequest request) { ... }
// PUT /api/users/{id}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
// DELETE /api/users/{id}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) { ... }
}

Parameter Binding

Parameter Annotations
@GetMapping("/search")
public Page<User> searchUsers(
@RequestParam(required = false) String name, // Query param ?name=john
@RequestParam(defaultValue = "0") int page, // With default
@RequestParam(defaultValue = "10") int size,
@RequestHeader("X-Request-ID") String requestId, // Header value
@CookieValue("session") String session // Cookie value
) { ... }
@PostMapping
public User create(@RequestBody @Valid CreateUserRequest request) { ... }
// Request body deserialized from JSON

4. Handler Mappings: Connecting URLs to Methods

HandlerMapping determines which controller method should handle a request. When your endpoint returns 404 unexpectedly, this is where to look.

Handler Mapping Resolution
Request: GET /api/users/123
┌────────────────────────────────────────────────┐
│ RequestMappingHandlerMapping scans: │
│ │
│ @GetMapping("/api/users/{id}") │
│ → Pattern: /api/users/{id} │
│ → Matches: /api/users/123 │
│ → Method: UserController#getUser(Long) │
└────────────────────────────────────────────────┘

Boot auto-configures RequestMappingHandlerMapping by scanning @Controller and @RestController classes.

I debug mapping issues by enabling debug logging:

application.yml
logging:
level:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: DEBUG

This shows every mapping registered at startup:

Terminal window
Mapped "{[/api/users],methods=[GET]}"
Mapped "{[/api/users/{id}],methods=[GET]}"
Mapped "{[/api/users],methods=[POST]}"

5. View Resolvers vs Message Converters

This distinction trips up many developers. MVC applications use ViewResolvers; REST APIs use MessageConverters.

ViewResolvers (Traditional MVC)

View Resolution
@GetMapping("/profile")
public String profile(Model model) {
model.addAttribute("user", currentUser);
return "profile"; // Logical view name
}
// ViewResolver resolves "profile" to:
// - /WEB-INF/views/profile.jsp (JSP)
// - templates/profile.html (Thymeleaf)

MessageConverters (REST APIs)

JSON Serialization
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id);
}
// MappingJackson2HttpMessageConverter converts User to JSON:
// {"id":123,"name":"John","email":"[email protected]"}

Boot auto-configures Jackson for JSON. To customize serialization:

User.java
public class User {
@JsonProperty("user_id")
private Long id;
@JsonProperty("full_name")
private String name;
@JsonIgnore // Don't serialize this
private String password;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate createdAt;
}

6. Exception Handling: Graceful Error Responses

Proper exception handling separates production-ready applications from toys. Spring MVC provides two main approaches.

Controller-Level Exception Handling

UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
"User with id " + ex.getUserId() + " not found"
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Global Exception Handling

GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex
) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse error = new ErrorResponse("VALIDATION_FAILED", errors);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericError(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred"
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}

7. Filters vs Interceptors: Request Processing Hooks

Both filters and interceptors process requests before they reach your controller, but at different levels.

Filters (Servlet Level)

Filters operate at the servlet container level, before Spring MVC processing.

JwtFilter.java
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && jwtUtil.validateToken(token)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
jwtUtil.extractUsername(token),
null,
jwtUtil.extractAuthorities(token)
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response); // Continue processing
}
}

Interceptors (Spring MVC Level)

Interceptors operate within Spring MVC, after DispatcherServlet receives the request.

LoggingInterceptor.java
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
request.setAttribute("startTime", System.currentTimeMillis());
return true; // Continue to controller
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex
) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
log.info("{} {} took {}ms",
request.getMethod(),
request.getRequestURI(),
duration
);
}
}

Key Differences

AspectFilterInterceptor
LevelServlet containerSpring MVC
AccessOnly HttpServletRequest/ResponseFull Spring context
Use CaseSecurity, CORSLogging, metrics
RegistrationFilterRegistrationBeanWebMvcConfigurer
Registering Interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoggingInterceptor loggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/health");
}
}

Summary

In this post, I explained the 7 essential Spring MVC concepts that every Spring Boot developer should master: DispatcherServlet, request lifecycle, controller annotations, handler mappings, view resolvers vs message converters, exception handling, and filters vs interceptors.

Spring Boot’s auto-configuration is powerful, but it creates a black box. When I understand what happens under the hood, I can debug faster, optimize better, and build more robust applications.

I recommend starting with Baeldung’s Spring MVC guides and the official Spring documentation. The time you invest in learning Spring MVC fundamentals will pay off every time something breaks in production.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments