The 30 second answer
I start by restating the requirements and agreeing the request and response shapes, then follow the codebase's existing layers: a thin controller with validation, a service that owns the business logic and transactions, and a repository for data access, with DTOs at the edge. I add pagination with a capped page size, map errors centrally to clear responses, check security and rate limits, and write a test for each endpoint, explaining each choice as I go.
Before the round. Clone, build and run the project and its tests, and fix anything environment-related then, not on the call. Find one existing endpoint and trace it end to end. Note which conventions exist already: DTOs or entities in responses, a global exception handler, the security configuration, the test style. Reusing them is faster and shows you can work in someone else's code.
In the round, in this order.
- Clarify. Restate the two APIs, the request and response JSON, status codes, and edge cases (not found, duplicates, empty results, invalid input). Agree what "done" means.
- Layers. Keep the controller thin: it maps HTTP to a method call and validates input. The service holds business rules and the
@Transactionalboundary. The repository only talks to the database. Return DTOs, not JPA entities, so the API contract does not change when the table does and lazy-loading surprises stay out of serialization. - Pagination. Accept
pageandsize, capsize, use a stable sort, and return page metadata. Mention keyset pagination for large or frequently changing tables. - Errors. Map exceptions in one
@RestControllerAdvice, returning consistent bodies. Spring Framework 6 supports the RFC 9457 problem-details format throughProblemDetailandResponseEntityExceptionHandler. - Security and limits. Say how the endpoint is authenticated and authorized (for example a JWT resource server and a role or ownership check), and where rate limiting belongs: often at the API gateway, or with a token-bucket filter in the service that returns
429 Too Many RequestswithRetry-After. - Tests. A controller slice test with
@WebMvcTestandMockMvcfor status codes and validation, a service unit test for the rules, and if time allows an integration test with@SpringBootTestor@DataJpaTest, using Testcontainers for a real database.
public record OrderDto(Long id, String status, BigDecimal total) {}
@RestController
@RequestMapping("/api/orders")
class OrderController {
private final OrderService service;
OrderController(OrderService service) { this.service = service; }
@GetMapping
PageResponse<OrderDto> list(@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size) {
return service.list(PageRequest.of(page, size, Sort.by("id").descending()));
}
@GetMapping("/{id}")
OrderDto get(@PathVariable Long id) { return service.get(id); }
}
@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail notFound(OrderNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
}Here PageResponse, OrderService and OrderNotFoundException stand for the codebase's own types. On Spring Framework 6.1 and later, constraint annotations such as @Min on controller method parameters trigger built-in method validation, raising HandlerMethodValidationException; on older versions you add a class-level @Validated instead. If the controller keeps a class-level @Validated, validation runs through an AOP proxy instead, and the reference says to remove that annotation to use the built-in support. Either way, a request with size=1000000 gets a 400 instead of a million-row query.
Configuration through properties. The same round asked about pointing the app at a local LLM endpoint. Bind settings to a typed class instead of hard-coding them, so each environment changes only its properties:
@ConfigurationProperties(prefix = "llm")
public record LlmProperties(URI baseUrl, String model, Duration timeout) {}
// application.properties
// llm.base-url=http://localhost:8081
// llm.model=local-model
// llm.timeout=10sEnable it with @ConfigurationPropertiesScan or @EnableConfigurationProperties(LlmProperties.class), and override per environment with profile-specific files or environment variables such as LLM_BASE_URL.
What the observers listen for. Small steps that keep the app compiling, a test run after each step, naming the trade-off when you choose one (for example "clamp or reject oversized pages? I'll reject with 400 so clients see the error"), and asking before changing shared code.
Sources (8)
- Spring Framework reference: Error responses and ProblemDetaildocs.spring.io
- Spring Framework reference: Controller advicedocs.spring.io
- RFC 9457: Problem Details for HTTP APIsrfc-editor.org
- Spring Framework reference: Validation in Spring MVC controllersdocs.spring.io
- Spring Framework reference: Java Bean Validationdocs.spring.io
- Spring Security reference: Authorize HTTP requestsdocs.spring.io
- Spring Boot reference: Testing Spring Boot applicationsdocs.spring.io
- Spring Boot reference: Externalized configurationdocs.spring.io