Hexagonal Architecture, also known as Ports and Adapters, is a design pattern that emphasizes separating the business logic from infrastructure and frameworks, making the application core independent of external dependencies. This approach allows you to plug and replace various parts of the system (e.g., databases, APIs, or UI frameworks) without changing the business logic.
In this article, we will explore:
What Hexagonal Architecture is.
The advantages of using Hexagonal Architecture.
A step-by-step implementation in Reactive Spring Boot.
What is Hexagonal Architecture?
Hexagonal Architecture aims to create loosely coupled components, where the core domain logic is surrounded by interfaces known as Ports and implemented by external components called Adapters.
Core Domain Logic: Represents the core business logic of your application.
Ports: Interfaces that expose core functionalities and facilitate interactions with external systems.
Primary Ports: Used by external systems to communicate with the application (e.g., REST APIs).
Secondary Ports: Used by the core logic to communicate with external systems (e.g., databases).
Adapters: Implementations that connect the ports to actual infrastructure or external systems.
Primary Adapters: Entry points for external clients.
Secondary Adapters: Implementations to connect the core to external resources like databases.
Advantages of Hexagonal Architecture
Independence: Business logic is separated from technology concerns.
Adaptability: Easily replace any component (e.g., swap out databases or web frameworks).
Testability: The independent nature makes testing easier by isolating logic from infrastructure.
Scalability: Loosely coupled architecture allows the system to scale without major restructuring.
Implementation of Hexagonal Architecture in Reactive Spring Boot
We’ll implement a sample Product Management service using Reactive Spring Boot with Hexagonal Architecture.
- Domain Layer: Core Logic The domain layer consists of entities and business logic. It is the heart of the system and doesn’t depend on any external technology.
Product Entity
`
public class Product {
private final String id;
private final String name;
private final double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and additional domain logic can be added here
}`
Product Repository Port (Interface)
This is the secondary port — an interface that defines operations the core logic requires for persistence.
`public interface ProductRepository {
Mono<Product> findById(String id);
Mono<Product> save(Product product);
}`
2.** Primary Adapter: REST API
The primary adapter serves as the entry point to interact with the domain logic. We’ll use a REST controller in Spring Boot to act as the primary adapter.
Product Controller
`
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable String id) {
return productService.getProductById(id)
.map(product -> ResponseEntity.ok(product))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<Product>> createProduct(@RequestBody Product product) {
return productService.createProduct(product)
.map(savedProduct -> ResponseEntity.status(HttpStatus.CREATED).body(savedProduct));
}
}`
Here, the controller acts as a primary adapter by exposing the product services to the user.
- Service Layer The service layer is responsible for interacting with the domain layer and serves as a bridge between adapters and the core logic.
Product Service
`
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Mono<Product> getProductById(String id) {
return productRepository.findById(id);
}
public Mono<Product> createProduct(Product product) {
return productRepository.save(product);
}
}`
ProductService relies on the ProductRepository port, which ensures that the domain logic remains independent from database implementations.
- Secondary Adapter: Persistence Layer The secondary adapter is responsible for the implementation of the persistence logic. We will use Reactive Spring Data for a non-blocking interaction with a MongoDB database.
Reactive Product Repository Adapter
`
@Repository
public class ProductReactiveRepositoryAdapter implements ProductRepository {
private final ReactiveMongoRepository<ProductDocument, String> mongoRepository;
public ProductReactiveRepositoryAdapter(ReactiveMongoRepository<ProductDocument, String> mongoRepository) {
this.mongoRepository = mongoRepository;
}
@Override
public Mono<Product> findById(String id) {
return mongoRepository.findById(id)
.map(document -> new Product(document.getId(), document.getName(), document.getPrice()));
}
@Override
public Mono<Product> save(Product product) {
ProductDocument document = new ProductDocument(product.getId(), product.getName(), product.getPrice());
return mongoRepository.save(document)
.map(savedDocument -> new Product(savedDocument.getId(), savedDocument.getName(), savedDocument.getPrice()));
}
}`
The ProductReactiveRepositoryAdapter is a secondary adapter. It maps the core Product to a MongoDB-compatible document and uses the reactive persistence APIs.
Product Document for MongoDB
`
@Document(collection = "products")
public class ProductDocument {
@Id
private String id;
private String name;
private double price;
public ProductDocument(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and setters
}`
Reactive Programming and Its Role in Hexagonal Architecture
Reactive Spring Boot uses non-blocking, asynchronous APIs, which make it suitable for applications that deal with many simultaneous requests or need to interact with external services without blocking.
Mono and Flux are used to handle reactive streams, ensuring non-blocking behavior across different layers of the application.
The ProductRepository port returns a Mono to represent asynchronous data retrieval.
Testing Hexagonal Architecture
Hexagonal Architecture makes testing easier by allowing you to test the core logic independently of external systems.
Unit Test for Product Service
We can mock the ProductRepository to test the ProductService.
`
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
public void testGetProductById() {
String productId = "123";
Product product = new Product(productId, "Test Product", 100.0);
when(productRepository.findById(productId)).thenReturn(Mono.just(product));
Mono<Product> result = productService.getProductById(productId);
StepVerifier.create(result)
.expectNext(product)
.verifyComplete();
}
}`
By mocking ProductRepository, the test focuses on the ProductService's behavior, allowing easy verification of core logic without involving actual database operations.
Conclusion:
Hexagonal Architecture allows building flexible and adaptable systems by decoupling business logic from external concerns. By using Reactive Spring Boot, we can leverage the non-blocking, asynchronous nature of reactive streams to create high-performance and scalable applications.
Hexagonal Architecture in Reactive Spring Boot enables:
Separation of Concerns: Core business logic remains isolated and unaffected by infrastructure changes.
Independence: Switching between different databases or external systems becomes easier by changing the adapter implementation.
Scalability and Performance: With a reactive approach, the system can handle more concurrent users and external connections effectively.
Top comments (0)