Spring @PathVariable vs @RequestParam: URL and Query Parameter Binding
Master Spring's @PathVariable and @RequestParam annotations.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Basic knowledge of Spring Boot and REST APIs.
- ✓Familiarity with Java annotations and dependency injection.
- ✓Understanding of HTTP request structure (path, query parameters).
- Use @PathVariable for required, unique identifiers in the URL path (e.g., /users/{id}).
- Use @RequestParam for optional query parameters, filters, or pagination (e.g., /users?page=1).
- @PathVariable binds to URI template variables; @RequestParam binds to query parameters or form data.
- Both support default values, required flags, and type conversion, but handle missing values differently.
- Never use @PathVariable for optional parameters – it will break your API contract.
Think of a URL like a house address. @PathVariable is like the street number – it's part of the path to get there. @RequestParam is like asking 'which floor?' or 'do you have a package?' – it's extra info after a question mark. You can't have a house without a street number, but you can skip the extra questions.
Every Spring developer has written a controller that takes parameters from the URL. But I've seen more production outages from misusing @PathVariable and @RequestParam than from any other annotation. It sounds simple, but the devil's in the details.
In this article, I'll share hard-won lessons from debugging a fintech startup's API that crashed under load because someone used @PathVariable for an optional filter. You'll learn exactly when to use each annotation, how to handle edge cases, and what the official docs gloss over.
We'll cover basic binding, advanced configuration, validation, and production debugging. By the end, you'll never confuse these two again – and your APIs will be more robust for it.
Understanding @PathVariable: Binding URL Template Variables
The @PathVariable annotation binds a method parameter to a URI template variable. It's the go-to for extracting values from the URL path itself. For example, in a REST API for a payment system, you might have:
@GetMapping("/payments/{paymentId}") public Payment getPayment(@PathVariable Long paymentId) { ... }
This is straightforward, but I've seen teams misuse it for optional parameters. Remember: path variables are always required by nature – they're part of the path. If you make them optional, you're breaking REST conventions.
Spring automatically converts the string value to the target type (e.g., Long, Integer, UUID). If conversion fails, you get a TypeMismatchException. Always use wrapper types (Long, Integer) instead of primitives to allow null in case of missing values – though missing path variables are rare, they can happen with misconfigured routes.
You can also use multiple path variables:
@GetMapping("/users/{userId}/payments/{paymentId}") public Payment getPayment(@PathVariable Long userId, @PathVariable Long paymentId) { ... }
Pro tip: If you're using Spring Boot 2.2+, the annotation value is inferred from the method parameter name if compiled with -parameters. Otherwise, you must specify the name explicitly: @PathVariable("paymentId").
Mastering @RequestParam: Query Parameters and Form Data
The @RequestParam annotation extracts values from query parameters (after ?) or form data. It's perfect for optional filters, pagination, and sorting. For example:
@GetMapping("/payments") public List<Payment> listPayments(@RequestParam(required = false, defaultValue = "0") int page, @RequestParam(required = false, defaultValue = "10") int size) { return paymentService.findAll(page, size); }
Key differences from @PathVariable: @RequestParam can be optional, has a default value, and supports multiple values (e.g., ?status=PENDING&status=COMPLETED). Use @RequestParam List<String> status to capture repeated parameters.
One common mistake: forgetting that @RequestParam is required by default. If a client omits the parameter, Spring throws MissingServletRequestParameterException. Always set required=false and provide a sensible default unless the parameter is truly mandatory.
Another gotcha: type conversion. If you expect an enum, Spring does automatic conversion if the enum has a String constructor or @JsonCreator. But for custom types, you'll need a Converter or PropertyEditor.
For form data, @RequestParam works the same way – it binds to form fields in POST requests. But for complex objects, use @ModelAttribute instead.
When to Use Which: A Decision Framework
Here's the hard truth: most teams get this wrong. They use @PathVariable for everything because it looks cleaner, or they use @RequestParam for everything because it's more flexible. Both are bad.
Use this decision framework:
- Is the parameter a unique identifier for a resource? Use @PathVariable.
- Example: /users/{userId} – userId uniquely identifies a user.
- Is the parameter optional, or does it filter/modify the response? Use @RequestParam.
- Example: /users?role=admin – role filters the list.
- Is the parameter part of the resource hierarchy? Use @PathVariable.
- Example: /users/{userId}/orders/{orderId} – orderId is nested under userId.
- Is the parameter a configuration or metadata? Use @RequestParam.
- Example: /users?includeAddress=true – includeAddress modifies the response shape.
- Is the parameter required for the request to make sense? If it's in the path, use @PathVariable. If it's in the query, use @RequestParam (required=true).
I once worked on a SaaS billing system where the team used @PathVariable for an optional 'include_discount' flag. The result? A bunch of URL patterns like /invoices/{id}/{includeDiscount} that were ugly and confusing. We refactored to use @RequestParam and the API became much cleaner.
Advanced Binding: Custom Converters and Validation
Spring's default type conversion works for primitives, strings, dates, and enums. But what if you have a custom type like a PaymentId value object? You need a Converter.
Create a class implementing org.springframework.core.convert.converter.Converter<String, PaymentId>:
public class PaymentIdConverter implements Converter<String, PaymentId> { @Override public PaymentId convert(String source) { return new PaymentId(source); // validate and parse } }
Then register it in a WebMvcConfigurer:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new PaymentIdConverter()); } }
Now you can use @PathVariable PaymentId paymentId directly.
Validation: Use Bean Validation (JSR-380) with @Validated on the controller class. For @RequestParam, add constraints like @Min, @Max, @Pattern on the method parameter:
@GetMapping("/payments") public List<Payment> listPayments( @RequestParam @Min(0) int page, @RequestParam @Max(100) @Min(1) int size) { ... }
This will throw ConstraintViolationException if validation fails. Handle it globally with @ExceptionHandler.
One gotcha: Spring's default error messages are ugly. Customize them with message attributes or a global exception handler that returns a consistent error JSON.
What the Official Docs Won't Tell You
The Spring Framework reference documentation is excellent, but it glosses over several real-world gotchas that I've encountered in production.
- Trailing Slash Ambiguity: A URL pattern like /users/{id}/ will match /users/123/ but also /users/ with an empty string for id. This can lead to subtle bugs. Always use regex patterns in @RequestMapping to enforce constraints: @GetMapping("/users/{id:[0-9]+}").
- Encoding Issues: If your path variable contains a slash (e.g., a date like 2023/12/01), Spring will reject it because the slash is a path separator. Use @RequestParam for such values, or encode the slash as %2F and configure Tomcat to allow encoded slashes: server.tomcat.internal-proxies=.* and set relaxedQueryChars.
- Optional @PathVariable with Map: You can use @PathVariable Map<String, String> to capture all path variables, but it's rarely needed. However, if you have optional path variables, this is the only way to handle them – and the docs don't emphasize that path variables are always required in the pattern.
- @RequestParam with Default Values and Type Conversion: If you set defaultValue="0" for an int parameter, but the client sends an empty string (?page=), Spring will use the default value. However, if you set defaultValue="" for a String, it becomes empty string, not null. This matters for validation.
- Performance Impact: In high-throughput systems, binding many @RequestParam parameters (e.g., 20+ filters) can be slow because Spring uses reflection to set each parameter. Consider using a single @ModelAttribute or a custom argument resolver for complex filters.
I learned these the hard way while debugging a payment gateway that failed under load because of path variable encoding issues. The client was sending dates with slashes, and we had to scramble to fix it.
Testing Parameter Binding: Integration Tests That Catch Issues
You should never trust that your parameter binding works without tests. Integration tests using MockMvc or WebTestClient are essential. Here's how to test both annotations:
For @PathVariable, test with valid and invalid path values:
mockMvc.perform(get("/api/users/123")) .andExpect(status().isOk());
mockMvc.perform(get("/api/users/abc")) // invalid type .andExpect(status().isBadRequest());
For @RequestParam, test with missing, empty, and multiple values:
mockMvc.perform(get("/api/payments")) .andExpect(status().isOk()); // should work with defaults
mockMvc.perform(get("/api/payments?page=1&size=abc")) .andExpect(status().isBadRequest()); // invalid size
Also test optional parameters: ensure that omitting them uses the default value.
One thing the docs don't stress: test your custom converters. If you have a PaymentId converter, test that it correctly parses valid strings and throws appropriate exceptions for invalid ones.
Use @WebMvcTest to load only the web layer, and mock the service layer. This keeps tests fast and focused.
The Case of the Missing Query Parameter
- Always explicitly set required and defaultValue for @RequestParam if the parameter can be missing.
- Use @PathVariable only for required path segments – never for optional parameters.
- Add global exception handling for MissingServletRequestParameterException to return a user-friendly error.
- Document your API contract clearly – clients need to know which parameters are mandatory.
- Write integration tests that omit optional parameters to catch missing defaults.
Check controller: @RequestParam(required=false, defaultValue="0") int pageCheck client request URL for missing parameter| File | Command / Code | Purpose |
|---|---|---|
| PaymentController.java | @RestController | Understanding @PathVariable |
| PaymentSearchController.java | @RestController | Mastering @RequestParam |
| DecisionExample.java | @GetMapping("/users/{userId}") | When to Use Which |
| CustomConverterExample.java | public class PaymentId { | Advanced Binding |
| TrailingSlashFix.java | @GetMapping("/users/{id:[0-9]+}") | What the Official Docs Won't Tell You |
| ParameterBindingTest.java | @WebMvcTest(UserController.class) | Testing Parameter Binding |
Key takeaways
Interview Questions on This Topic
What is the difference between @PathVariable and @RequestParam in Spring MVC?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't