leanmind logo leanmind text logo

Blog

TDD Avanzado

Las herramientas que se necesitan para aplicar TDD en el mundo real en cualquier proyecto.

Arquitectura Hexagonal en Spring

Por Aitor Santana

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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.

Fuentes

Hexagonal Architecture, DDD, Spring

Publicado el 12/03/2024 por

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers