Table of contents
- Introduction
- Definition of Hexagonal Architecture
- Hexagonal vs Clean Architecture
- Core Concepts of Hexagonal Architecture
- Ports
- Adapters
- Domain Model
- Application Services
- Setting Up a Java Spring Boot Project
- Prerequisites and Tools Needed
- Initializing the Project
- Implementing Hexagonal Architecture with Java Spring Boot
- Developing the Domain Layer
- Developing the Application Layer
- Designing the Tech Framework Layer
- Conclusion
- References
If your software has been running for over 10 years on an outdated framework or database, and you're looking to upgrade to a modern solution but still want to keep the core business code, this is when the significance of a well-designed architecture becomes clear.
Introduction
Definition of Hexagonal Architecture
An architectural pattern which creating loosely coupled application components that can be easily connected to their software environment by means of ports and adapters. This makes components exchangeable at any level.
Hexagonal vs Clean Architecture
Hexagonal or Clean architecture have the same objective, which is the separation of concerns.
Divide the software into layers
Independent of frameworks, UI, database, external services
Testable without UI, database, web server, or any other external services
You can see the similarity between them.
Core Concepts of Hexagonal Architecture
Ports
Port is an interface layer that define how core domain layer interact with external components (frameworks, external services, databases,…).
Inbound ports: handle input to the core, such as user commands or requests from clients or other systems.
Outbound ports: define how the core can communicate with external systems like databases or APIs.
Adapters
Adapters are the implementations of ports.
Inbound Adapters: These include things like REST controllers, CLI interfaces, or event-driven consumers that handle user input or external events.
Outbound Adapters: These are responsible for communicating with external systems, such as databases, third-party services, or message brokers.
Domain Model
This is the core domain layer where holds domain entities and business logic. It’s isolated from frameworks and external components and is the most stable part of the software.
Application Services
An application service acts as a facade through which clients interact with the domain model. This is where ports are defined.
It control database transactions, orchestrates business operations but should not make any business decisions (should be in domain layer).
Setting Up a Java Spring Boot Project
Prerequisites and Tools Needed
Java 17
Spring Boot 3
Intelij or any Java IDE
Initializing the Project
We start with https://start.spring.io/ to create a new Spring Boot framework. I chose Springweb and Lombok dependencies only for the demo.
You can check the project structure on the website before downloading it to your local.
Implementing Hexagonal Architecture with Java Spring Boot
I will implement a booking service where we can create a booking as a sample.
To implement 3 layers of the architecture, I created 3 Java modules:
Domain: store domain entities and business logic
Application: store inbound and outbound ports
Tech Framework: includes frameworks and adapters for database, controllers, external services,…
In which, Application module depends on Domain module:
and Tech Framework module depends on both Application and Domain:
As you can see, the domain has no dependency on frameworks or external components.
Developing the Domain Layer
Firstly, I have a BookingDomainEntity (just a sample, ignore the attributes pls :D)
import java.time.LocalDateTime;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@ToString
public class BookingDomainEntity {
private long id;
private long userId;
private long totalAmount;
private String pickUpAddress;
private String dropOffAddress;
private LocalDateTime createdOn;
}
Secondly, I have commands for business logic. To keep it simple, I don’t use command for booking creation.
Developing the Application Layer
As I showed you, we have inbound and outbound ports in this layer.
To creat a booking, I have a BookingCommandInboundPort so that it can be called by controllers from Tech Framework layer.
@Slf4j
public class BookingCommandInboundPort {
private final BookingRepositoryOutboundPort bookingRepositoryOutboundPort;
public BookingCommandInboundPort(BookingRepositoryOutboundPort bookingRepositoryOutboundPort) {
this.bookingRepositoryOutboundPort = bookingRepositoryOutboundPort;
}
public void save(BookingDomainEntity entity) {
log.info("Saving BookingDomainEntity");
bookingRepositoryOutboundPort.save(entity);
}
}
And a BookingRepositoryOutboundPort for saving a new booking to database:
public interface BookingRepositoryOutboundPort {
List<BookingDomainEntity> findAll();
void save(BookingDomainEntity entity);
}
Then the application service should look like this:
Designing the Tech Framework Layer
Finally, we implement the outsidemost layer. The dependencies that I chose previously is for this layer. I also added mapstruct library for object mapping.
We also have inbound and outbound structure similar to the application layer.
Firstly, I have BookingEntity and BookingRepository for persistence. To keep it simple, I don’t use database and only create a dummy entity and repository:
// should have @Entity in real project
@Data
@Builder
public class BookingEntity {
private long id;
private long userId;
private long totalAmount;
private String pickUpAddress;
private String dropOffAddress;
private LocalDateTime createdOn;
}
@Repository // Dummy repository => should be an interface and extend JpaRepository
public class BookingRepository {
List<BookingEntity> findAll() {
return List.of(BookingEntity.builder().build());
}
BookingEntity save(BookingEntity entity) {
// dummy save method
return entity;
}
}
Secondly, I have a BookingRepositoryAdapter that implement BookingRepositoryOutboundPort (from application layer):
@Component
@RequiredArgsConstructor
public class BookingRepositoryAdapter implements BookingRepositoryOutboundPort {
private final BookingRepository bookingRepository;
private final BookingMapper mapper;
@Override
public List<BookingDomainEntity> findAll() {
List<BookingEntity> entities = bookingRepository.findAll();
return mapper.toBookingDomainEntity(entities);
}
@Override
public void save(BookingDomainEntity entity) {
// Transform Aggregate Root 's states into JPA Entity 's states before saving them into database
bookingRepository.save(mapper.toBookingJPAEntity(entity));
}
}
and a BookingMapper for object mapping. I strongly recommend you try Mapstruct to eliminate these mapping boilerplate code.
@Mapper(componentModel = "spring")
public interface BookingMapper {
@Mapping(target = "createdOn", ignore = true)
BookingDomainEntity toBookingDomainEntity(BookingRequest request);
BookingEntity toBookingJPAEntity(BookingDomainEntity entity);
List<BookingDomainEntity> toBookingDomainEntity(List<BookingEntity> entities);
}
Ok finally, we need a BookingController to handle client requests:
@RestController
@RequestMapping("/api/v1/bookings")
@RequiredArgsConstructor
public class BookingController {
// Inbound port in application layer
private final BookingCommandInboundPort bookingCommandInboundPort;
private final BookingMapper mapper;
@PostMapping(
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<BookingResponse> createBooking(@RequestBody BookingRequest request) {
bookingCommandInboundPort.save(mapper.toBookingDomainEntity(request));
return ResponseEntity.ok().build();
}
}
If you are feeling lost in this architecture, you are not alone :D. The flow is quite complex and can be confusing. Don't worry, I’ve sketched out a request flow so you can better visualize the entire process we just discussed:
From this perspective, it's clear that if I want to replace my Spring Boot with Quarkus in the future, I can retain the application and domain modules since they are not framework-dependent. This will save a lot of effort, trust me! 😊
Conclusion
We've just covered the definition and a hands-on example of Hexagonal Architecture. You might not fully understand it after reading, but I recommend trying out the sample first. Then, when you come back, I believe things will be clearer for you.
Next time, I will implement a DDD sample in this repo and we will discuss about why we should apply Hexagonal along with DDD for our project.
See yaa!!!