← Artefactos
ISIS2603 · Capa de Servicios

Lógica de Negocio

Spring Boot · Servicios, validaciones y reglas de negocio

¿Dónde vive la lógica de negocio?

Nuestra aplicación se organiza en tres capas. La capa de lógica es el corazón que conecta la interfaz con los datos.

🌐 Capa API REST

Controladores — @RestController
Responsabilidad: Recibe solicitudes HTTP y las traduce a llamadas al servicio. Trabaja con DTOs y JSON. No conoce la base de datos directamente.

⚙ Capa de Lógica de Negocio

Servicios — @Service
Responsabilidad: Coordina el flujo de la aplicación, valida reglas de negocio e invoca la persistencia. Trabaja exclusivamente con objetos @Entity. No conoce DTOs, JSON ni HTTP.

🗄 Capa de Persistencia

Repositorios — JpaRepository
Responsabilidad: Gestiona el acceso a la base de datos. Ofrece métodos CRUD automáticos y permite definir consultas personalizadas. Spring Data JPA genera la implementación automáticamente.
Principio clave: El Backend no conoce DTOs ni JSON ni ninguna otra representación de los recursos. Solo trabaja con entidades (@Entity).

¿Qué es un @Service?

En Spring, las clases de lógica de negocio se anotan con @Service. Esta anotación le indica al contenedor de Spring que esa clase es un bean — un objeto cuyo ciclo de vida es gestionado por el framework.

La convención de nombrado es usar el sufijo Service. Por ejemplo: BookService, AuthorService, EditorialService.

¿Qué objetos viajan entre capas?

Las clases de la lógica reciben y retornan objetos @Entity — las mismas clases que representan las tablas en la base de datos. Los controladores se encargan de convertir entre Entity y DTO, pero el servicio nunca toca esa conversión.

Responsabilidades de la capa de lógica

  • Validar las reglas de negocio antes de persistir datos
  • Coordinar la comunicación entre el API y la persistencia
  • Invocar servicios externos cuando sea necesario (ej: validar un ISBN)
  • Lanzar excepciones cuando las reglas no se cumplen

Inyección de Dependencias

Cómo Spring conecta automáticamente las capas de tu aplicación con @Autowired.

¿Qué es la inyección de dependencias?

Imagina que tienes una clase BookService que necesita acceder a la base de datos mediante BookRepository. En lugar de que tú crees manualmente la instancia del repositorio con new BookRepository(), Spring lo hace por ti.

Cuando declaras una variable con @Autowired, le dices a Spring: "busca en tu contenedor un bean de este tipo e inyéctalo aquí". Spring resuelve la dependencia y conecta los objetos automáticamente en tiempo de ejecución.

@RestController
BookController
@Autowired
@Service
BookService
@Autowired
JpaRepository
BookRepository

La lógica recibe la persistencia inyectada

El servicio declara una dependencia al repositorio. Spring se encarga de crear y entregar la instancia.

BookService.java
@Service public class BookService {   @Autowired   private BookRepository repository;   @Transactional   public List<BookEntity> getBooks()   {     return repository.findAll();   } }

El Controlador recibe la lógica inyectada

El controlador depende del servicio. No necesita conocer cómo se instancia ni sus dependencias internas.

BookController.java
@RestController @RequestMapping("/books") public class BookController {   @Autowired   private BookService bookService;   @GetMapping   public List<BookDTO> getAll()   {     // convierte Entity → DTO aquí     return bookService.getBooks()...;   } }
¿Por qué importa? La inyección de dependencias permite que cada capa se desacople de las demás. El servicio no sabe cómo funciona el repositorio internamente y el controlador no sabe cómo funciona el servicio. Esto facilita el testing, el mantenimiento y la evolución de la aplicación.

🗄 ¿Qué es un Repository?

Un Repository es una interfaz que extiende JpaRepository. Spring Data JPA genera automáticamente la implementación con métodos CRUD como findAll(), findById(), save() y deleteById().

Además, puedes definir consultas personalizadas por convención de nombrado:

BookRepository.java
public interface BookRepository     extends JpaRepository<BookEntity, Long> {   // Spring genera SQL automáticamente   List<BookEntity> findByName(String name);   List<BookEntity> findByIsbn(String isbn); }

Reglas de Negocio

La capa de lógica valida las condiciones que deben cumplirse antes de persistir o modificar datos. Haz clic en cada regla para ver su implementación.

1 No puede haber dos compañías con el mismo nombre

Antes de crear o actualizar una compañía, consultamos la base de datos para verificar si ya existe otra con ese nombre. Si existe, lanzamos una excepción.

CompanyService.java — createCompany()
@Transactional public CompanyEntity createCompany(         CompanyEntity entity)   throws IllegalOperationException {   // 1. Consultar la persistencia   List<CompanyEntity> alreadyExist =       repository.findByName(entity.getName());   // 2. Validar la regla de negocio   if (!alreadyExist.isEmpty()) {     throw new IllegalOperationException(         "Ya existe una compañía con ese nombre");   }   // 3. Si pasa, persistir   return repository.save(entity); }
Patrón: consulta → valida → persiste. Si la validación falla, se lanza una excepción y la operación se cancela.
2 El ISBN del libro debe ser válido (no vacío, no nulo)

Antes de crear un libro, verificamos que el campo ISBN no esté vacío ni sea nulo. Este es un ejemplo de validación de campo simple.

BookService.java — createBook()
@Transactional public BookEntity createBook(BookEntity book)   throws IllegalOperationException {   // Validar que el ISBN no sea vacío   if (book.getIsbn() == null         || book.getIsbn().isEmpty()) {     throw new IllegalOperationException(         "El ISBN no puede ser vacío");   }   return repository.save(book); }
3 La fecha de publicación debe ser posterior al nacimiento del autor

Este ejemplo muestra una regla que involucra datos de dos entidades relacionadas. Necesitamos consultar tanto el libro como su autor para validar.

BookService.java — validación cruzada
// Validar coherencia de fechas if (book.getPublishDate() != null       & author.getBirthDate() != null) {   if (book.getPublishDate()         .before(author.getBirthDate()))   {     throw new IllegalOperationException(         "La fecha de publicación no puede "         + "ser anterior al nacimiento del autor");   } }
Nota: Las validaciones cruzadas entre entidades son comunes en reglas de negocio. El servicio tiene acceso a múltiples repositorios para resolver estas dependencias.
4 Un empleado no puede tener un salario superior a 50 millones

Ejemplo de una regla con validación numérica directa sobre un atributo de la entidad.

EmployeeService.java
private static final Double MAX_SALARY = 50000000.0; @Transactional public EmployeeEntity createEmployee(         EmployeeEntity emp)   throws IllegalOperationException {   if (emp.getSalary() > MAX_SALARY)   {     throw new IllegalOperationException(         "Salario excede el máximo permitido");   }   return repository.save(emp); }
5 No puede haber dos departamentos con el mismo nombre en la misma compañía

Esta regla es contextual: la unicidad del nombre depende de la compañía padre. Necesitamos una consulta personalizada en el repositorio.

DepartmentRepository.java
// Consulta derivada por convención List<DepartmentEntity> findByCompanyIdAndName(   Long companyId, String name );
DepartmentService.java
@Transactional public DepartmentEntity createDepartment(         Long companyId, DepartmentEntity dept)   throws IllegalOperationException {   List<DepartmentEntity> existing =       deptRepo.findByCompanyIdAndName(           companyId, dept.getName());   if (!existing.isEmpty())   {     throw new IllegalOperationException(         "Ya existe un departamento con ese nombre "         + "en esta compañía");   }   return deptRepo.save(dept); }
6 La editorial debe existir al crear un libro

Al crear un libro, este debe estar asociado a una editorial válida. Verificamos que el ID de la editorial proporcionada exista realmente en la base de datos antes de guardar el libro.

BookService.java — createBook()
@Transactional public BookEntity createBook(         BookEntity bookEntity)   throws IllegalBusinessLogicException {   // 1. Verificar que venga la info de la editorial   if (bookEntity.getEditorial() == null)     throw new IllegalBusinessLogicException(         "La editorial no es válida");   // 2. Buscar la editorial en la BD   Optional<EditorialEntity> editorial =       editorialRepository.findById(           bookEntity.getEditorial().getId());   // 3. Validar existencia   if (editorial.isEmpty()) {     throw new IllegalBusinessLogicException(         "La editorial no existe");   }   // 4. Asignar y persistir   bookEntity.setEditorial(editorial.get());   return bookRepository.save(bookEntity); }
Integridad Referencial: No podemos guardar un registro "hijo" (Libro) si el registro "padre" (Editorial) no existe.
Patrón general: Toda regla de negocio sigue el mismo flujo: consultar datos existentes → evaluar la condición → lanzar excepción si no se cumple → persistir si todo está correcto.

Verifica tu comprensión

Responde estas preguntas para confirmar que entendiste los conceptos clave de la capa de lógica.

1. ¿Qué anotación identifica a una clase como servicio de lógica de negocio en Spring?
@RestController
@Service
@Entity
@Repository
¡Correcto! @Service indica que la clase es un bean gestionado por Spring que contiene lógica de negocio.
No exactamente. @RestController es para la capa API, @Entity para el modelo de datos, y @Repository para la persistencia. La respuesta correcta es @Service.
2. ¿Qué hace la anotación @Autowired?
Crea una nueva tabla en la base de datos
Define una ruta HTTP para un endpoint
Le indica a Spring que inyecte automáticamente una dependencia
Marca un método como transaccional
¡Correcto! @Autowired delega al contenedor de Spring la resolución e inyección de la dependencia en tiempo de ejecución.
No es correcto. @Autowired es exclusivamente para inyección de dependencias — le dice a Spring que busque un bean compatible y lo asigne a esa variable.
3. ¿Qué tipo de objetos maneja la capa de lógica (servicio)?
DTOs y JSON
Entidades (@Entity)
HttpServletRequest y HttpServletResponse
Strings directamente
¡Correcto! El Backend trabaja exclusivamente con @Entity. Los DTOs y JSON son responsabilidad de la capa de controladores (API REST).
Recuerda: el Backend no conoce DTOs, JSON ni HTTP. Solo trabaja con objetos @Entity, las clases que representan las tablas en la base de datos.
4. En una prueba unitaria, ¿qué se espera cuando se viola una regla de negocio?
Que el método retorne null
Que la base de datos ignore la operación
Que se lance una excepción (IllegalOperationException)
Que se registre un log de warning
¡Correcto! Se usa assertThrows(IllegalOperationException.class, () -> {...}) para verificar que la excepción se lance correctamente.
Cuando una regla de negocio se viola, el servicio debe lanzar una IllegalOperationException, y en la prueba se verifica con assertThrows.
5. ¿Por qué usamos @BeforeEach y los métodos clearData/insertData en las pruebas?
Para conectar con la base de datos de producción
Para garantizar que cada test se ejecute bajo las mismas condiciones iniciales
Para evitar usar Podam
Porque JUnit obliga a definir estos métodos
¡Correcto! Como las pruebas no se ejecutan en un orden específico, el setUp garantiza un estado limpio y predecible antes de cada test.
El setUp con @BeforeEach existe para que cada test parta de un estado limpio y conocido, eliminando dependencias entre pruebas.
6. ¿Qué ocurre si se lanza una RuntimeException dentro de un método anotado con @Transactional?
Se guardan los datos parcialmente hasta donde llegó el código.
Se hace un rollback automático de todos los cambios en la BD.
La aplicación se detiene y se cierra.
Se ignora la excepción y el método retorna null.
¡Correcto! La anotación @Transactional garantiza la atomicidad: si algo falla, todo se revierte (rollback).
Incorrecto. La característica principal de las transacciones es la atomicidad. Si ocurre una excepción no controlada, Spring fuerza un rollback para no dejar datos inconsistentes.
7. ¿Cómo debe acceder un Servicio a la base de datos?
Creando una conexión JDBC manualmente.
Instanciando la clase repositorio con 'new Repository()'.
Inyectando una interfaz Repository con @Autowired.
Llamando directamente a la base de datos desde el Controlador.
¡Exacto! Usamos Inyección de Dependencias para delegar la persistencia a los Repositorios definidos en Spring Data.
No es correcto. En Spring, los componentes (Beans) se gestionan por el contenedor. Debes inyectar el repositorio (usando constructor o @Autowired), nunca usar 'new' ni conexiones manuales en esta capa.