Arquitectura Hexagonal en Spring
12-03-2024
Arquitectura Hexagonal en Spring
En estas semanas he estado redescubriendo Java, con el objetivo de pulir las bases y practicar muchos de los conceptos
comunes del desarrollo. Además también empecé a aprender uno de sus frameworks más conocidos, Spring Boot.
Para ello, decidí hacer una pequeña API para un market-place en el que tuviera 4-5 entidades, para ir probando un poco
el funcionamiento.
También quería aprender un poco de arquitectura, por lo que decidí implementar la API siguiendo una arquitectura
hexagonal.
Mi idea era tener el siguiente esquema:
src/main/java/com/sstark/generalmarket/
|-- application/
| |-- services/
| |
| |-- repositories/
| |
|-- domain/
| |-- models/
| |
|-- infrasctructure/
| |-- adapters/
| |
| |-- configuration/
| |
| |-- entities/
| |
| |-- repositories/
| |
| |-- controllers/
De esta estructura lo más importante es la carpeta de configuración y los diferentes adaptadores, que serán los
repositorios que inyecte Spring en los servicios.
Veamos paso a paso el proceso para que funcione todo correctamente:
Implementación
Estos ejemplos serán en base a una entidad producto que tengo en la API.
Lo primero que haremos será crear nuestra entidad de Spring:
infrastructure/entities/ProductEntity.java
package com.sstark.generalmarket.infrastructure.entities;
import jakarta.persistence.*;
@Entity
@Table(name = "Product")
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Integer productId;
private String name;
@Column(name = "category_id")
private Integer categoryId;
private String barcode;
@Column(name = "sale_price")
private Double salePrice;
private Integer stock;
private Boolean state;
@ManyToOne
@JoinColumn(name = "category_id", insertable = false, updatable = false)
private Category category;
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public String getBarcode() {
return barcode;
}
public void setBarcode(String barcode) {
this.barcode = barcode;
}
public Double getSalePrice() {
return salePrice;
}
public void setSalePrice(Double salePrice) {
this.salePrice = salePrice;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Boolean getState() {
return state;
}
public void setState(Boolean state) {
this.state = state;
}
}
Lo siguiente será crear un repositorio en la capa de infraestructura, propio de Spring Data:
infrastructure/repositories/ProductJpaRepository.java
package com.sstark.generalmarket.infrastructure.repositories;
import com.sstark.generalmarket.domain.models.Product;
import com.sstark.generalmarket.infrastructure.entities.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProductJpaRepository extends JpaRepository<ProductEntity, Integer> {}
Una vez tenemos la parte de infraestructura, creamos un repositorio en la capa de dominio, con los métodos que nos interese implementar:
application/repositories/ProductRepository.java
package com.sstark.generalmarket.domain.repositories;
import com.sstark.generalmarket.domain.models.MarketPage;
import com.sstark.generalmarket.domain.models.Product;
import java.util.List;
import java.util.Optional;
public interface ProductRepository {
List<Product> findAll();
/*...*/
}
Lo siguiente, será crear un adaptador que tenga como dependencia el repositorio de JPA, e implemente el repositorio de dominio. Además, declararemos la clase con la anotación @Component, para que Spring lo detecte como un bean y pueda inyectarlo en su servicio correspondiente.
infrastructure/adapters/PostgresProductAdapter.java
package com.sstark.generalmarket.infrastructure.adapters;
import com.sstark.generalmarket.domain.models.Product;
import com.sstark.generalmarket.domain.repositories.ProductRepository;
import com.sstark.generalmarket.infrastructure.repositories.ProductJpaRepository;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class PostgresProductRepository implements ProductRepository {
private final ProductJpaRepository repository;
private final ProductMapper mapper;
private final PageMapper pageMapper;
/** Los mappers los usaré para hacer transformaciones
de entidad a dominio y viceversa
*/
public PostgresProductRepository(ProductJpaRepository repository, ProductMapper mapper, PageMapper pageMapper) {
this.repository = repository;
}
@Override
public List<Product> findAll() {
return repository.findAll();
}
}
Para poder desacoplarnos de los repositorios que requiere Spring Data, y no usar anotaciones de Spring en la capa de aplicación, debemos crear un fichero de configuración en el que registraremos nuestros servicios como beans de Spring, para que se produzca la inyección de dependencias.
infrastructure/configuration/BeanConfiguration.java
package com.sstark.generalmarket.infrastructure.configuration;
import com.sstark.generalmarket.application.services.ProductService;
import com.sstark.generalmarket.domain.repositories.ProductRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.sstark.generalmarket")
public class BeanConfiguration {
@Bean
ProductService productService(final ProductRepository productRepository) {
return new ProductService(productRepository);
}
}
Creamos el servicio en el que vamos a inyectar nuestro repositorio de dominio:
application/services/ProductService.java
package com.sstark.generalmarket.application.services;
import com.sstark.generalmarket.domain.models.Product;
import com.sstark.generalmarket.domain.repositories.ProductRepository;
import java.util.List;
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> findAll() {
return productRepository.findAll();
}
}
Ya solo nos queda exponer un controlador que utilice nuestro servicio:
infrastructure/controllers/ProductController.java
package com.sstark.generalmarket.infrastructure.controllers;
import com.sstark.generalmarket.application.services.ProductService;
import com.sstark.generalmarket.domain.models.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/all")
public ResponseEntity<List<Product>> products() {
return new ResponseEntity<>(productService.findAll(), HttpStatus.OK);
}
}
Con este último paso, hemos conseguido implementar arquitectura hexagonal en Spring de forma exitosa.
Seguiré usando este proyecto en un futuro para aprender más cosas acerca del ecosistema de este framework.
Dejo el repositorio por aquí para que podáis ver el código y la
evolución de la API.