Building REST APIs, versioning strategies, REST best practices, ResponseEntity, DELETE status codes, Swagger/OpenAPI, embedded servers and how Boot picks one, switching servers, the must-know annotations, and @Component vs @Service/@Repository.
Published September 25, 2026
For REST questions, back up each claim with an HTTP detail: the status code, the header, the method semantics. Interviewers also love the annotation list, so know what each annotation does and the one trap attached to it.
Short answer:
spring-boot-starter-web.@RestController, and map methods with @GetMapping, @PostMapping, @PutMapping, @PatchMapping and @DeleteMapping.@PathVariable, @RequestParam and @RequestBody, adding @Valid for validation.ResponseEntity with the correct status code.@RestController
@RequestMapping("/api/v1/products")
class ProductController {
private final ProductService service;
ProductController(ProductService service) { this.service = service; }
@GetMapping
Page<ProductDto> list(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return service.list(PageRequest.of(page, size));
}
@PostMapping
ResponseEntity<ProductDto> create(@Valid @RequestBody CreateProductRequest req) {
ProductDto created = service.create(req);
return ResponseEntity.created(URI.create("/api/v1/products/" + created.id())).body(created); // 201 + Location
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable long id) { service.delete(id); }
}
Key points to cover:
Learn it in depth → REST Controllers
Short answer: Versioning lets you make breaking changes without breaking existing clients. Several versions run side by side while clients migrate. There are four common strategies:
| Strategy | Example | Notes |
|---|---|---|
| URI path | /api/v1/orders | Most common; visible, easy to route and cache |
| Query parameter | /api/orders?version=1 | Simple, but easy to forget |
| Custom header | X-API-Version: 1 | Keeps URLs clean; less visible |
| Media type (content negotiation) | Accept: application/vnd.shop.v1+json | Most "RESTful"; harder to test in a browser |
Key points to cover:
Deprecation and Sunset headers.@GetMapping(version = "1.1")).Learn it in depth → API Contract Design
Short answer:
/orders/{id}/items), not verbs.Key points to cover:
Location), 204 No Content.ResponseEntity used for?Short answer: It represents the whole HTTP response, so you control the status code, headers and body in one return value. For example: 201 with a Location header, 204 with no body, or conditional 304 responses with an ETag.
@GetMapping("/{id}")
ResponseEntity<OrderDto> get(@PathVariable long id) {
return service.findOptional(id)
.map(order -> ResponseEntity.ok().eTag(order.version()).body(order))
.orElse(ResponseEntity.notFound().build());
}
Key points to cover:
ResponseEntity (or @ResponseStatus) when you need anything else.Short answer:
Key points to cover:
Short answer: Swagger is the tooling ecosystem around the OpenAPI Specification, the standard, machine-readable description of REST APIs (endpoints, parameters, schemas, authentication). Swagger UI renders that description as interactive documentation, where developers can try the endpoints.
Key points to cover:
springdoc-openapi-starter-webmvc-ui). It generates the spec at /v3/api-docs, and serves the UI at /swagger-ui.html. Springfox is unmaintained.Short answer: springdoc builds the OpenAPI document from your code: mappings, DTOs, validation annotations. That keeps the documentation in sync with the implementation. The same document can generate client SDKs, drive contract tests, and be imported into API gateways and Postman.
@Operation(summary = "Get an order by id")
@ApiResponse(responseCode = "404", description = "Order not found")
@GetMapping("/{id}")
OrderDto get(@PathVariable long id) { … }
Key points to cover:
springdoc.swagger-ui.enabled=false).Short answer: Tomcat (the default, with spring-boot-starter-web), Jetty and Undertow for servlet applications, and Reactor Netty as the default for WebFlux.
Short answer: It doesn't depend on classpath order. The servlet web-server auto-configuration imports the Tomcat, Jetty and Undertow configurations in that fixed order. Each is guarded by @ConditionalOnClass (is this server present?) and @ConditionalOnMissingBean(ServletWebServerFactory.class). So the first matching one wins: Tomcat if it's present, otherwise Jetty, otherwise Undertow. You can force a choice by defining your own ServletWebServerFactory bean.
Common trap: the popular answer "whichever comes first in the dependency list" is wrong. The priority is fixed by the auto-configuration's import order and conditions.
Short answer: Exclude Tomcat from the web starter, and add the other server's starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Short answer: Grouped by purpose:
| Annotation | What it does | Trap or tip |
|---|---|---|
@SpringBootApplication | @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan | Put it in the root package |
@EnableAutoConfiguration | Turns on conditional auto-configuration | Already included above |
@Configuration / @Bean | Declare beans in Java config | @Bean for third-party objects |
@ComponentScan | Where to look for components | Defaults to the class's package and its sub-packages |
@Component, @Service, @Repository, @Controller | Stereotypes for auto-detected beans | @Repository adds exception translation |
@RestController | @Controller + @ResponseBody | Returns data, not views |
@RequestMapping / @GetMapping… | Map requests to handler methods | Class-level base path |
@PathVariable, @RequestParam, @RequestBody, @ResponseBody | Bind request parts and write the response | @RequestBody uses Jackson |
@Autowired | Inject dependencies | Unnecessary on a single constructor |
@EnableWebMvc | Imports Spring MVC's full configuration | In Boot it switches off MVC auto-configuration. Usually don't add it |
@EnableAsync + @Async | Run methods on a task executor | Self-invocation bypasses the proxy |
@EnableScheduling + @Scheduled | Cron, fixed-rate or fixed-delay jobs | Runs on every instance; use a lock (ShedLock) in clusters |
Learn it in depth → Component Scanning & Configuration
@Component instead of @Service and @Repository? Then why use them?Short answer: Yes. Technically, all three register a bean through component scanning. The specialised stereotypes still matter:
@Repository activates persistence exception translation, which turns vendor-specific exceptions (SQLException, Hibernate exceptions) into Spring's DataAccessException hierarchy.@Service and @Repository document the architectural layer. Tools, AOP pointcuts and architecture tests (ArchUnit) can target layers by annotation.Q: What's the difference between PUT and PATCH? A: PUT replaces the whole resource with the request body, and is idempotent. PATCH applies a partial update, for example with a JSON Merge Patch. It isn't inherently idempotent.
Q: How do you make POST requests safe to retry?
A: Accept an Idempotency-Key header, store the key together with the result of the first request, and return the stored result for any repeat. That's essential for payments and order creation.
Q: How do you implement pagination in Spring Boot?
A: Accept a Pageable parameter (for example ?page=0&size=20&sort=createdAt,desc), and return a Page<T>, or a trimmed DTO with the content plus page metadata. For very large or fast-changing datasets, prefer cursor (keyset) pagination.
Q: What does @Async need in order to work?
A: @EnableAsync, a public method called from another bean (so the call goes through the proxy), and ideally a configured TaskExecutor. Return void or CompletableFuture<T>, and handle exceptions, because they don't reach the caller otherwise.