Capítulo 1: La Capa Service en Detalle
Introducción
La capa Service es el elemento central de la arquitectura de 3 capas. Hace el enlace entre el controlador (que gestiona las solicitudes HTTP) y el repository (que accede a los datos). Aquí es donde reside la lógica de negocio de su aplicación.
Objetivos del Capítulo
Al final de este capítulo, usted será capaz de:
- Comprender el rol y las responsabilidades de la capa Service
- Separar correctamente las preocupaciones entre las capas
- Implementar una capa Service profesional
- Utilizar la inyección de dependencias correctamente
- Aplicar buenas prácticas
Arquitectura de 3 Capas
Visión General
Responsabilidades de Cada Capa
| Capa | Responsabilidades | Ejemplos |
|---|---|---|
| Controller | Recepción HTTP, Validación, Conversión, Respuestas | @RestController, @GetMapping |
| Service | Lógica de negocio, Orquestación, Transacciones, Validación de reglas | @Service, Cálculos |
| Repository | Acceso a datos, Consultas BD, Mapeo de entidades | @Repository, JpaRepository |
¿Qué es la Lógica de Negocio?
Definición
La lógica de negocio (business logic) es el conjunto de reglas y procesos que definen el comportamiento de su aplicación.
Ejemplos de Lógica de Negocio
❌ NO es lógica de negocio:
// Esto es acceso a datos
productRepository.findById(id);
// Esto es gestión HTTP
return ResponseEntity.ok(product);
✅ Lógica de negocio:
// Verificar el stock antes de crear una orden
if (product.getStock() < order.getQuantity()) {
throw new InsufficientStockException();
}
// Calcular el precio total con impuestos
double totalPrice = order.getSubtotal() * (1 + TAX_RATE);
// Aplicar una promoción
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9; // 10% de descuento
}
Creación de una Capa Service
Ejemplo Básico
package com.example.demo.service;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
// Inyección por constructor (RECOMENDADO)
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// CREATE
public Product createProduct(Product product) {
// Lógica de negocio: validación
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
// Llamada al repository
return productRepository.save(product);
}
// READ - Todos
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// READ - Por ID
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
}
// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
// Lógica de negocio: validación
if (productDetails.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
// Actualización de campos
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
// DELETE
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
Puntos Clave
- @Service: Marca la clase como una capa Service
- Inyección por constructor: Más limpio y testeable que @Autowired en los campos
- Validación de negocio: Verificación de reglas antes del acceso a datos
- Gestión de errores: Lanzar excepciones apropiadas
Inyección de Dependencias
Tres Métodos de Inyección
1. Inyección por Constructor (RECOMENDADA)
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Ventajas:
- ✅ Inmutabilidad (final)
- ✅ Fácil de testear
- ✅ Obligatoria (error de compilación si falta)
- ✅ Recomendado por Spring
2. Inyección por Campo (NO RECOMENDADA)
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
}
Desventajas:
- ❌ Difícil de testear
- ❌ Dependencias ocultas
- ❌ No es inmutable
3. Inyección por Setter (RARAMENTE USADA)
@Service
public class ProductService {
private ProductRepository productRepository;
@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Separación de Preocupaciones
Principio SOLID: Responsabilidad Única
Cada capa debe tener UNA sola responsabilidad.
❌ Mal Ejemplo: Todo en el Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@PostMapping
public Product create(@RequestBody Product product) {
// Lógica de negocio en el controller (¡MAL!)
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
// Acceso directo al repository (¡MAL!)
return productRepository.save(product);
}
}
Problemas:
- ❌ Lógica de negocio en el controller
- ❌ Difícil de testear
- ❌ Difícil de reutilizar
- ❌ Violación del principio de responsabilidad única
✅ Buen Ejemplo: Separación Clara
// Controller: Solo gestión HTTP
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public Product create(@RequestBody Product product) {
return productService.createProduct(product);
}
}
// Service: Lógica de negocio
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product createProduct(Product product) {
// Validación de negocio
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
// Llamada al repository
return productRepository.save(product);
}
}
// Repository: Acceso a datos
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring genera automáticamente la implementación
}
Ejemplo Avanzado: Orquestación
La capa Service puede coordinar varios repositories:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
}
public Order createOrder(Long customerId, Long productId, int quantity) {
// 1. Verificar que el cliente existe
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));
// 2. Verificar que el producto existe
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
// 3. Verificar el stock
if (product.getStock() < quantity) {
throw new InsufficientStockException("Stock insuffisant");
}
// 4. Calcular el precio total
double totalPrice = product.getPrice() * quantity;
// 5. Aplicar un descuento si el cliente es premium
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9;
}
// 6. Crear la orden
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);
// 7. Disminuir el stock
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// 8. Guardar la orden
return orderRepository.save(order);
}
}
Gestión de Transacciones
Anotación @Transactional
@Service
public class OrderService {
@Transactional
public Order createOrder(Long customerId, Long productId, int quantity) {
// Todas las operaciones están en una transacción
// Si se lanza una excepción, todo se revierte (rollback)
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setStock(product.getStock() - quantity);
productRepository.save(product);
Order order = new Order();
// ... creación de la orden
return orderRepository.save(order);
}
}
Ventajas:
- ✅ Consistencia de los datos
- ✅ Rollback automático en caso de error
- ✅ ACID (Atomicidad, Consistencia, Aislamiento, Durabilidad)
Buenas Prácticas
DO (A Hacer)
✅ Inyección por constructor
public ProductService(ProductRepository repository) {
this.repository = repository;
}
✅ Métodos con nombres explícitos
public Product createProduct(Product product)
public void activateUser(Long userId)
public double calculateTotalPrice(Order order)
✅ Validación de negocio en el Service
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
✅ Gestión de excepciones apropiadas
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
DON'T (A Evitar)
❌ Lógica de negocio en el Controller
// MAL: En el controller
@PostMapping
public Product create(@RequestBody Product product) {
if (product.getPrice() < 0) { // ¡Lógica de negocio!
throw new Exception();
}
return service.create(product);
}
❌ Acceso directo al Repository desde el Controller
// MAL
@RestController
public class ProductController {
@Autowired
private ProductRepository repository; // ¡Directo!
}
❌ Inyección por campo
// MAL
@Autowired
private ProductRepository repository;
Ejercicios Prácticos
Ejercicio 1: Service Básico
Objetivo: Crear una capa Service completa
Instrucciones:
- Cree una entidad
Student:- Long id
- String firstName
- String lastName
- String email
- int grade
- Cree
StudentRepository - Cree
StudentServicecon:- Todas las operaciones CRUD
- Validación: grade entre 0 y 100
- Validación: email válido
- Cree
StudentControllerque utilice el service
Ejercicio 2: Orquestación
Objetivo: Coordinar varios repositories
Instrucciones:
- Cree
Library(id, name, address) - Cree
Book(id, title, author, libraryId) - Cree
LibraryServicecon un método:public Library createLibraryWithBooks(Library library, List<Book> books)- Crear la biblioteca
- Crear todos los libros asociados
- Todo dentro de una transacción
Resumen del Capítulo
Puntos Clave
- La capa Service contiene la lógica de negocio
- Separación de preocupaciones: Controller → Service → Repository
- Inyección por constructor (recomendada)
- @Transactional para la consistencia de los datos
- Validación en el Service, no en el Controller
Lo que Usted Sabe Ahora
✅ Rol y responsabilidades de la capa Service
✅ Separación de preocupaciones
✅ Inyección de dependencias
✅ Orquestación de varios repositories
✅ Buenas prácticas profesionales