Five essential REST annotations (and @RequestMapping/@GetMapping/@PostMapping/@PathVariable), resolving overlapping and "ambiguous" mappings without changing URL or method, @RequestParam vs @PathVariable vs @ModelAttribute, multiple matching @ExceptionHandlers, custom statuses, content negotiation, HiddenHttpMethodFilter, global interception without filters, designing a custom exception-handling framework (Problem Details), creating custom annotations, securing DTOs, plus follow-ups on PUT vs POST bugs, @Qualifier conflicts, file uploads and cache refresh.
Published September 25, 2026
Senior MVC questions are about how the DispatcherServlet picks and invokes handlers, and how you design consistent API behaviour: errors, content types, validation. Know the extension points: HandlerMapping, HandlerAdapter, argument resolvers, message converters, HandlerInterceptor, and @ControllerAdvice.
Short answer:
@RestController: @Controller + @ResponseBody, so return values are serialised to the response body (JSON through Jackson).@RequestMapping: maps a path (and optionally the method, consumes, produces, params or headers) to a class or method. Usually used at class level for a base path.@GetMapping/@PostMapping/@PutMapping/@PatchMapping/@DeleteMapping: method-specific shortcuts for @RequestMapping(method = …).@PathVariable: binds a URI template variable (/orders/{id}).@RequestBody: deserialises the body. @RequestParam binds query or form parameters. @RequestHeader, @ResponseStatus, and @Valid/@Validated (validation) are also used constantly.@RestController
@RequestMapping("/api/v1/orders")
class OrderController {
@GetMapping("/{id}")
OrderDto get(@PathVariable UUID id) { return service.get(id); }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
OrderDto create(@Valid @RequestBody CreateOrderRequest req, @RequestHeader("Idempotency-Key") String key) {
return service.create(req, key);
}
}
Learn it in depth → Building REST Controllers
@RequestMapping, @GetMapping, @PostMapping and @PathVariable each do, precisely?Short answer:
@RequestMapping: registers RequestMappingInfo (paths, methods, params, headers, consumes, produces) with RequestMappingHandlerMapping. Class-level and method-level mappings are combined.@GetMapping: a composed annotation, equivalent to @RequestMapping(method = GET). It's for reads: safe and idempotent.@PostMapping: @RequestMapping(method = POST). It's for creation or commands: not idempotent unless you design for it.@PathVariable: resolved by PathVariableMethodArgumentResolver from the matched URI template. It supports regex constraints ({id:\\d+}), required = false with optional paths, and type conversion.Short answer: The classic bug: creation over POST, retried by a client or gateway after a timeout, which creates duplicate orders or payments, because POST isn't idempotent. Fixes:
Idempotency-Key, and the server stores the key and the result, and returns the same result on retries);PUT /orders/{clientGeneratedId}), where repeated calls update the same resource;The opposite bug is using PUT for partial updates, which wipes fields the client didn't send. Use PATCH, or require the full representation, with ETags.
@Autowired and @Qualifier? What goes wrong if they're mixed up?Short answer: Injecting by type with several candidates throws NoUniqueBeanDefinitionException. Resolve it with:
@Qualifier("stripeGateway") at the injection point (matching a bean name or a custom qualifier annotation);@Primary on the default implementation;Map<String, PaymentGateway> or List, and selection by key (the Strategy registry).The pitfalls:
NoSuchBeanDefinitionException at startup (it fails fast, which is good);@Primary silently wins where a specific bean was intended, if someone forgets the qualifier;@Bean methods vs on class-level components that don't match;@Stripe), which are type-safe.@RestController complicate things?Short answer: @RestController is the norm for upload APIs: @PostMapping(consumes = MULTIPART_FORM_DATA_VALUE) with @RequestPart MultipartFile file plus metadata. Configure spring.servlet.multipart.max-file-size/max-request-size, stream the file to storage (S3) instead of holding it in memory, validate the type and size, and scan for malware.
Where complications arise:
byte[] from a @RestController loads everything into memory. Use ResponseEntity<Resource> or StreamingResponseBody, with explicit Content-Disposition, Content-Type and Content-Length headers.@Controller, with selective @ResponseBody.Short answer: For example, product details and prices, cached with Caffeine plus Redis, using cache-aside:
@CacheEvict when a product is updated. Better still, after the transaction commits (@TransactionalEventListener), so readers never re-cache stale data.ProductUpdated (Kafka, or Redis pub/sub) so every node evicts its local copy.refreshAfterWrite), which avoids latency spikes.product:v2:{id}) when the cached format changes.@RequestMappings overlap? How can two "ambiguous" URLs work without changing the URL or the HTTP method?Short answer: RequestMappingHandlerMapping collects every mapping whose conditions match, then picks the most specific, comparing:
/orders/latest beats /orders/{id}, and a literal beats a variable;If two remain equally specific, startup (or the request) fails with "Ambiguous handler methods mapped".
To disambiguate the same URL and method without changing either, add request conditions:
params: @GetMapping(path = "/reports", params = "type=summary") vs params = "type=detail", or params = "!type";headers: headers = "X-API-Version=2";consumes/produces: the same POST path, consuming application/json vs multipart/form-data, or producing application/json vs text/csv (content negotiation through the Accept header);/{id:\\d+} vs /{slug:[a-z-]+}.@GetMapping(path = "/items/{key}", produces = MediaType.APPLICATION_JSON_VALUE) ItemDto asJson(@PathVariable String key) { ... }
@GetMapping(path = "/items/{key}", produces = "text/csv") String asCsv(@PathVariable String key) { ... }
@GetMapping(path = "/items/{id:\\d+}") ItemDto byId(@PathVariable long id) { ... }
@RequestParam, @PathVariable and @ModelAttribute?Short answer:
@PathVariable: a value inside the URI path (/orders/{id}). It identifies a resource, and is required by default.@RequestParam: a query string or form field (?page=2&status=PAID). It's for filters, options and pagination. It supports defaultValue, required = false, and multiple values (List<String>).@ModelAttribute: binds many request parameters onto an object (OrderSearch with page, status and dateFrom fields), through data binding. It's also used on methods that add attributes to the model for views. For GET search forms, binding query parameters to a DTO keeps controller signatures clean. Add @Valid for validation.Request bodies (JSON) use @RequestBody, not @ModelAttribute.
@ExceptionHandler methods match the same exception?Short answer: Spring picks the handler for the closest match in the exception's type hierarchy: the most specific exception class declared. For example, EntityNotFoundException beats RuntimeException, which beats Exception. Precedence across locations:
@ControllerAdvice handlers;@Order/Ordered decides (the first matching advice wins);Two handlers for exactly the same exception type in one class gives IllegalStateException: Ambiguous @ExceptionHandler method at startup. You can also scope advice with @RestControllerAdvice(basePackages = …, assignableTypes = …, annotations = …).
@RestController method? How?Short answer: Several ways:
ResponseEntity: ResponseEntity.status(HttpStatus.ACCEPTED).header("Location", url).body(dto), which is dynamic per call.@ResponseStatus(HttpStatus.CREATED) on the method, for a fixed status.@ResponseStatus on a custom exception class, or ResponseStatusException (throw new ResponseStatusException(HttpStatus.CONFLICT, "version mismatch")).@ExceptionHandler returning ProblemDetail or ResponseEntity, which gives centralised mapping (the best for consistency).HttpServletResponse and calling setStatus (rarely needed).Short answer:
ContentNegotiationManager determines the requested media types, primarily from the Accept header (a URL suffix and a format query parameter can be enabled, and are off or deprecated by default).produces and the formats the registered HttpMessageConverters can write: Jackson JSON (the default), Jackson XML (if jackson-dataformat-xml is on the classpath), JAXB, String, and byte arrays.Content-Type selects the reader converter, or returns 415 Unsupported Media Type.Customise it with WebMvcConfigurer.configureContentNegotiation/extendMessageConverters, or by adding converters (for example CSV or Protobuf).
HiddenHttpMethodFilter, and why is it used for forms?Short answer: HTML forms only support GET and POST. HiddenHttpMethodFilter lets a form POST a hidden field _method=PUT|PATCH|DELETE. The filter wraps the request, so Spring MVC sees the intended method, and routes it to @PutMapping/@DeleteMapping handlers. It's used in server-rendered applications (Thymeleaf). In Boot, it's disabled by default (spring.mvc.hiddenmethod.filter.enabled=true turns it on). REST clients and SPAs send real HTTP methods, so they don't need it.
Short answer:
HandlerInterceptor (registered through WebMvcConfigurer.addInterceptors, with path patterns): preHandle (it can reject), postHandle and afterCompletion. It runs inside the DispatcherServlet, and knows the chosen handler. Good for authentication checks, locale, timing, and MDC setup.@ControllerAdvice with @InitBinder/@ModelAttribute (runs before handler methods), or RequestBodyAdvice/ResponseBodyAdvice, to inspect or modify bodies (for example, wrapping responses, or signing them).@Around("within(@RestController *)")).WebFilter or HandlerFilterFunction.Filters vs interceptors: servlet filters run before Spring MVC, for every request, including static resources and errors. They're the right place for security (Spring Security is a filter chain), CORS, compression, and request logging of raw traffic.
Short answer:
ApplicationException, carrying a stable error code (ORDER_NOT_FOUND), with subclasses for NotFound, Conflict, BusinessRuleViolation and ExternalServiceFailure. Don't put HTTP status codes in the domain layer. Map them at the edge.@RestControllerAdvice (ordered), mapping:
MethodArgumentNotValidException, ConstraintViolationException, HandlerMethodValidationException) to 400, with field errors;ProblemDetail, and spring.mvc.problemdetails.enabled=true), with type, title, status, detail, instance, plus extensions: errorCode, traceId, and errors[].@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler { // gets Problem Details for MVC exceptions
@ExceptionHandler(ApplicationException.class)
ProblemDetail handle(ApplicationException ex, HttpServletRequest req) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(ex.status(), ex.getMessage());
pd.setTitle(ex.code().title());
pd.setProperty("errorCode", ex.code().name());
pd.setProperty("traceId", Span.current().getSpanContext().getTraceId());
return pd;
}
@ExceptionHandler(Exception.class)
ProblemDetail unexpected(Exception ex) {
log.error("Unhandled error", ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error");
}
}
Learn it in depth → Global Exception Handling
Short answer: Declaring it is easy:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // must be RUNTIME to be visible to Spring or reflection
@Documented
public @interface RateLimited {
int permitsPerMinute() default 60;
String key() default ""; // for example, a SpEL expression for the per-user key
}
An annotation does nothing by itself. Give it behaviour through one of these:
@Around("@annotation(rateLimited)"));HandlerInterceptor that reads HandlerMethod.getMethodAnnotation(...);BeanPostProcessor (scanning beans at startup);@Constraint(validatedBy = ...));@Transactional(readOnly = true) @Retention(RUNTIME) @interface ReadOnlyTx {}, which Spring honours through its merged-annotation support).Choose @Target and @Retention deliberately, and add @Inherited or @Repeatable only when needed.
Short answer: DTOs are the boundary contract, so secure them on the way in and out:
role or balance). Include only the fields the client may set.FAIL_ON_UNKNOWN_PROPERTIES).@JsonView with care).toString and logs (records print all their fields by default, so override toString).@JsonTypeInfo with allow-lists.Q: What does the DispatcherServlet do, in order?
A: It finds a handler through the HandlerMappings, runs the interceptors' preHandle, invokes the handler through a HandlerAdapter (argument resolvers, then the method, then return-value handlers and message converters), runs postHandle, handles exceptions through HandlerExceptionResolvers (including @ExceptionHandler), renders views if any, and finally runs afterCompletion.
Q: How do you add a custom argument type to controller methods?
A: Implement HandlerMethodArgumentResolver (for example, resolving a CurrentUser parameter from the security context), and register it through WebMvcConfigurer.addArgumentResolvers.
Q: How do you version REST APIs in Spring MVC?
A: With a path prefix (/api/v1), a header condition (headers = "X-API-Version=2"), or media-type versioning (produces = "application/vnd.acme.v2+json"). Spring Framework 7 adds first-class API versioning support.
Q: @RestControllerAdvice vs @ControllerAdvice?
A: @RestControllerAdvice = @ControllerAdvice + @ResponseBody, so handler return values are written as response bodies (JSON), which suits REST APIs.