Skip to main content

Documentation 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.

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 the 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

com.zegel.springboot/
├── DemoApplication.java              # Entry point (@SpringBootApplication)
├── controller/
│   └── ProductController.java        # REST endpoints at /api/v1/products
├── service/
│   ├── ProductService.java           # Interface (business logic contract)
│   ├── ProductServiceBDImpl.java     # @Primary — in-memory repository implementation
│   └── ProductServiceApiExternaImpl.java  # Alternative external API implementation
├── repository/
│   └── ProductRepository.java        # HashMap-backed data access layer
├── entity/
│   └── Product.java                  # Domain model (id, name, price)
├── dto/
│   ├── ProductRequestDTO.java        # Validated inbound request body
│   └── ProductResponseDTO.java       # Outbound response shape
├── response/
│   └── ApiResponse.java              # Generic response envelope ApiResponse<T>
└── exception/
    ├── ProductNotFoundException.java
    └── GlobalExceptionHandler.java   # @ControllerAdvice for all exceptions

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.
@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    public ResponseEntity<ApiResponse<ProductResponseDTO>> createProduct(
            @Valid @RequestBody ProductRequestDTO dto) {
        ProductResponseDTO product = productService.saveProduct(dto);
        ApiResponse<ProductResponseDTO> response = new ApiResponse<>(
            String.valueOf(HttpStatus.CREATED.value()),
            "Producto creado correctamente",
            product
        );
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}
The controller never references the concrete service class — it only depends on the 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:
public interface ProductService {

    List<ProductResponseDTO> getAllProducts();
    ProductResponseDTO getProductById(Long id);
    ProductResponseDTO saveProduct(ProductRequestDTO product);
    ProductResponseDTO updateProduct(Long id, ProductRequestDTO product);
    void deleteProduct(Long id);
    List<ProductResponseDTO> filter(String name, Double minPrice, Double maxPrice);
}
All method signatures use DTOs (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:
@Repository
public class ProductRepository {

    private final Map<Long, Product> productData = new HashMap<>();
    private Long idCounter = 1L;

    public Product save(Product product) {
        if (product.getId() == null) {
            product.setId(idCounter++);
        }
        productData.put(product.getId(), product);
        return product;
    }

    public Optional<Product> findById(Long id) {
        return Optional.ofNullable(productData.get(id));
    }

    public List<Product> findAll() {
        return new ArrayList<>(productData.values());
    }

    public void deleteById(Long id) {
        productData.remove(id);
    }
}
Because the store is in-memory, all data is lost when the application restarts. This makes the repository ideal for local development and demos without requiring any external database configuration.

Entity

Product is the domain model. Lombok’s @Getter and @Setter annotations generate all accessor methods at compile time:
@Setter
@Getter
public class Product {
    private Long id;
    private String name;
    private Double price;
}

Strategy Pattern

The Strategy Pattern is the key design principle of the service layer. Two classes implement ProductService, 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:
@Service
@Primary
public class ProductServiceBDImpl implements ProductService {

    private final ProductRepository productRepository;

    public ProductServiceBDImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // ... full CRUD delegating to productRepository
}

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:
@Service
public class ProductServiceApiExternaImpl implements ExternalApiProductService {

    @Override
    public List<Product> getAllProducts() {
        return Arrays.asList(
            new Product(1L, "External Product 1", 10.0),
            new Product(2L, "External Product 2", 20.0),
            new Product(3L, "External Product 3", 30.0)
        );
    }
}
To switch the active implementation, move @Primary from ProductServiceBDImpl to a class that implements ProductService directly.

Request/response flow

Every inbound write operation follows this flow:
  1. ProductController receives a @Valid @RequestBody ProductRequestDTO
  2. Jakarta Bean Validation enforces @NotBlank, @Size(min=3, max=100), and @Min(value=1) on the DTO fields
  3. The controller calls the injected ProductService method, passing the DTO
  4. ProductServiceBDImpl maps the DTO to a Product entity, interacts with ProductRepository, then maps the result back to ProductResponseDTO
  5. The controller wraps ProductResponseDTO in ApiResponse<T> and returns a ResponseEntity

ProductRequestDTO — validated input

@Setter
@Getter
public class ProductRequestDTO {

    @NotBlank(message = "Nombre del proudcto es requerido")
    @Size(min = 3, max = 100, message = "El nombre del producto debe tener entre 3 y 100 caracteres")
    private String name;

    @Min(value = 1, message = "El precio del producto debe ser mayor a 0")
    private Double price;
}

ApiResponse<T> — generic response envelope

Every endpoint, including error responses, returns this wrapper:
@Setter
@Getter
public class ApiResponse<T> {

    private String responseCode;
    private String responseMessage;
    private T data;

    public ApiResponse(String responseCode, String responseMessage, T data) {
        this.responseCode = responseCode;
        this.responseMessage = responseMessage;
        this.data = data;
    }
}
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:
ExceptionHTTP statusDescription
ProductNotFoundException404 Not FoundThrown by the service when no product exists for a given ID
MethodArgumentNotValidException400 Bad RequestThrown by Jakarta Validation when a DTO constraint fails; field-level errors are included in data
Exception (catch-all)500 Internal Server ErrorAny unexpected runtime exception
All three handlers return an ApiResponse<T> envelope, ensuring clients always receive the same structure regardless of the error type.

Build docs developers (and LLMs) love