Hexagonal architecture is an application design pattern. It solves some problems of the layered architecture by introducing ports-and-adapter for the dependencies between our components of the application toward our domain objects. The domain objects are the core part of the application and it is the part of inside a hexagon. And other parts such as web interface, DB, messaging systems, etc are outside of a hexagon.
Hexagonal Architecture Diagram
Let’s discuss in details each of the stereotypes in this architecture style.
Core application part
Domain Objects are the core parts of an application. These have business rules and validations and also have state and behaviour. This core application part doesn’t have any outward dependency. These are pure core business logic services. Domain objects will be changed when the business requirement will be changed otherwise they never affect the changes in other layers.
Let’s see the following domain class Account of the core application, it has account-related information and business validations.
public class Account{
private String accountHolderName;
private Long accountNumber;
private String bankName;
// Constructors , Getters/Setters etc.
}
Inbound and outbound ports
In the Hexagonal architecture pattern, the ports provide the flow to the application from outside and inside.
Inbound ports
An inbound port provides the flow and the application functionality to the outside. An inbound port is a service interface that exposes the core logic and can be called by outside components. You can see the following example of an inbound port:
public interface AccountService {
void createAccount(Account account);
Account getAccount(Long accountNumber);
List<Account> allAccounts();
}
Outbound ports
An outbound port provides the outside functionality or interface. The core application calls this output port as per requirement such as external database call etc. For example, a simple repository interface AccountRepository that provides a port to enable communication from the core application to a database. This simple repository interface is an outbound port. Let’s see the following example of an outbound port:
public interface AccountRepository {
void createAccount(Account account);
Account getAccount(Long accountNumber);
List allAccounts();
}
Adapters
Adapters are nothing but these are the implementation of inbound and outbound ports. The adapters from the outside of the hexagonal architecture and they are not part of the core application. They only interact with the core application from outside by using inbound and outbound ports.
Input adapters
The input adapters are also known as primary or driving adapters. These drive the application by invoking actions on the application using the inbound ports of application.
For example, the AccountController provides REST APIs or web interfaces as the input adapters. The REST controllers use the service interfaces (inbound ports) to interact with the core part of the business logic of the application.
@RestController
@RequestMapping("/account")
public class AccountController{
@Autowired private AccountService accountService;
@PostMapping
public void createAccount(@RequestBody Account account) {
accountService.createAccount(account);
}
@GetMapping("/{accountNumber}")
public Account getAccount(@PathVariable Long accountNumber) {
return accountService.getAccount(accountNumber);
}
@GetMapping
public List<Account> allAccounts() {
return accountService.allAccounts();
}
}
Output adapters
The output adapters are also known as secondary or driven adapters. These are the implementation of the outbound ports. These are driven by the core application using the outbound ports to find the connections to the database and external APIs.
For example, the AccountRepositoryImpl provides an interface to the core application to communicate to external dependency such as the database.
@Repository
public class AccountRepositoryImpl implements AccountRepository {
private Map<Long, Account> accountDB = new HashMap<Long, Account>();
@Override
public void createAccount(Account account) {
accountDB.put(account.getAccountNumber(), account);
}
@Override
public Account getAccount(Long accountNumber) {
return accountDB.get(accountNumber);
}
@Override
public List<Account> allAccounts() {
return accountDB.values().stream().collect(Collectors.toList());
}
}
Use cases of a core application
In the Hexagonal architecture, the use cases and business domain objects are inside of the hexagonal. The use cases are nothing but it is specific use case implementation of the inbound port to communication from the core to the downstream system. Let’s see the following use case implementation AccountServiceImpl provides a use case for a specific requirement:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountRepository accountRepository;
@Override
public void createAccount(Account account) {
accountRepository.createAccount(account);
}
@Override
public Account getAccount(Long accountNumber) {
return accountRepository.getAccount(accountNumber);
}
@Override
public List<Account> allAccounts() {
return accountRepository.allAccounts();
}
}
Conclusion
In the article, we have discussed the Hexagonal application architecture with a quick example in Java. This architecture focuses to simplify application design with external and internal dependencies.