Nuestra aplicación se organiza en tres capas. La capa de lógica es el corazón que conecta la interfaz con los datos.
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.
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.
Cómo Spring conecta automáticamente las capas de tu aplicación con @Autowired.
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.
El servicio declara una dependencia al repositorio. Spring se encarga de crear y entregar la instancia.
@Service
public class BookService
{
@Autowired
private BookRepository repository;
@Transactional
public List<BookEntity> getBooks()
{
return repository.findAll();
}
}El controlador depende del servicio. No necesita conocer cómo se instancia ni sus dependencias internas.
@RestController
@RequestMapping("/books")
public class BookController
{
@Autowired
private BookService bookService;
@GetMapping
public List<BookDTO> getAll()
{
// convierte Entity → DTO aquí
return bookService.getBooks()...;
}
}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:
public interface BookRepository
extends JpaRepository<BookEntity, Long>
{
// Spring genera SQL automáticamente
List<BookEntity> findByName(String name);
List<BookEntity> findByIsbn(String isbn);
}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.
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.
@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);
}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.
@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);
}Este ejemplo muestra una regla que involucra datos de dos entidades relacionadas. Necesitamos consultar tanto el libro como su autor para validar.
// 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");
}
}Ejemplo de una regla con validación numérica directa sobre un atributo de la entidad.
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);
}Esta regla es contextual: la unicidad del nombre depende de la compañía padre. Necesitamos una consulta personalizada en el repositorio.
// Consulta derivada por convención
List<DepartmentEntity> findByCompanyIdAndName(
Long companyId, String name
);@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);
}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.
@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);
}
Responde estas preguntas para confirmar que entendiste los conceptos clave de la capa de lógica.