The Spring Boot Products API follows a classic four-layer architecture: an HTTP controller delegates work to a service interface, the service implementation reads and writes through a repository, and the repository maps data to and from theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ricpalomino/spring-boot/llms.txt
Use this file to discover all available pages before exploring further.
Product entity. This separation of concerns keeps each class focused on a single responsibility and makes the codebase straightforward to test and extend.
Package structure
Layers
Controller
ProductController is annotated with @RestController and @RequestMapping("/api/v1/products"). It accepts HTTP requests, delegates all business logic to ProductService, and wraps results in ApiResponse<T> before returning a ResponseEntity.
ProductService interface. Spring injects the correct implementation at startup based on which bean carries @Primary.
Service interface
ProductService defines the full contract for product business logic:
ProductRequestDTO / ProductResponseDTO), not the raw Product entity. This keeps the HTTP layer decoupled from the persistence model.
Repository
ProductRepository is the data access layer. It stores products in a HashMap<Long, Product> and manages its own auto-incrementing ID counter:
Entity
Product is the domain model. Lombok’s @Getter and @Setter annotations generate all accessor methods at compile time:
Strategy Pattern
The Strategy Pattern is the key design principle of the service layer. Two classes implementProductService, and Spring injects the correct one based on the @Primary annotation.
The
@Primary annotation on ProductServiceBDImpl tells Spring to use this bean whenever a ProductService dependency is declared. Removing @Primary from one class and adding it to the other is all you need to switch the active data source — no controller or configuration changes required.ProductServiceBDImpl (default)
Marked @Primary, this implementation delegates to ProductRepository for all data operations. It includes SLF4J logging and throws ProductNotFoundException when a product is not found:
ProductServiceApiExternaImpl (alternative)
This implementation satisfies ExternalApiProductService (a sibling interface) and simulates responses from an external API. It is not active by default because it does not carry @Primary:
@Primary from ProductServiceBDImpl to a class that implements ProductService directly.
Request/response flow
Every inbound write operation follows this flow:ProductControllerreceives a@Valid @RequestBody ProductRequestDTO- Jakarta Bean Validation enforces
@NotBlank,@Size(min=3, max=100), and@Min(value=1)on the DTO fields - The controller calls the injected
ProductServicemethod, passing the DTO ProductServiceBDImplmaps the DTO to aProductentity, interacts withProductRepository, then maps the result back toProductResponseDTO- The controller wraps
ProductResponseDTOinApiResponse<T>and returns aResponseEntity
ProductRequestDTO — validated input
ApiResponse<T> — generic response envelope
Every endpoint, including error responses, returns this wrapper:
data is typed as T, so GET /api/v1/products returns ApiResponse<List<ProductResponseDTO>>, while POST /api/v1/products returns ApiResponse<ProductResponseDTO>.
Global error handling
GlobalExceptionHandler is annotated with @ControllerAdvice and intercepts three exception types:
| Exception | HTTP status | Description |
|---|---|---|
ProductNotFoundException | 404 Not Found | Thrown by the service when no product exists for a given ID |
MethodArgumentNotValidException | 400 Bad Request | Thrown by Jakarta Validation when a DTO constraint fails; field-level errors are included in data |
Exception (catch-all) | 500 Internal Server Error | Any unexpected runtime exception |
ApiResponse<T> envelope, ensuring clients always receive the same structure regardless of the error type.