Skip to main content

Step 8

Step 8 — Spring MVC request boundaries and failure contracts

2 views

Table of contents

A working controller is not yet a complete API boundary. Putting request parsing, authentication, validation, business rules, transactions, and external calls in one method mixes failure causes and test responsibilities.

Keep the controller thin

The controller translates HTTP into business input and translates the result back into HTTP. The service owns business invariants and transactions; repositories hide persistence details. Trust the authenticated principal instead of a user ID supplied by the client, and reject conflicting resource IDs in the path and body.

@PostMapping("/items")
ResponseEntity<ItemResponse> create(
    @AuthenticationPrincipal UserPrincipal principal,
    @Valid @RequestBody CreateItemRequest request) {
  var result = itemService.create(principal.id(), request.toCommand());
  return ResponseEntity.status(201).body(ItemResponse.from(result));
}

Errors are part of the API contract

Return stable, distinct error codes for validation, unauthenticated access, forbidden access, missing resources, state conflicts, and dependency timeouts. Never return stack traces or raw provider bodies. Consider idempotency or duplicate-prevention keys for writes, and put deadlines plus bounded retries around external calls.

Responsibility boundaries

Controller

HTTP mapping, validation result, status code

Service

business invariants, authorization, transaction

Repository/client

SQL, external protocol, timeout

Completion evidence

  • Success, validation, denial, conflict, and timeout responses are documented.
  • Controller tests and real-database integration tests have separate purposes.
  • A slow dependency cannot occupy request threads without a bound.
  • Error responses expose no secret, SQL, or stack trace.

Related term: Spring MVC

Terms in this content

🎉 You finished Backend with Spring Boot 4

What's next? Pick another course below.