Hexagonal (Port and Adapter) Architecture
The Hexagonal architecture divides the system into loosely-coupled interchangeable components; such as application core, user interface, data repositories, test scripts, and other system interfaces etc.
In this approach, the applitcation core contains the business domain information and it is surrounded by a layer that contains adapters handling bi-directional communication with other components in a generic way.
The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as the need, not being constrained by a one-dimensional layered drawing. The term hexagonal architecture comes from this visual effect.
Why use Hexagonal Architecture?
Hexagonal architecture helps to make the business (domain) layer independent) from the framework, UI, database, or any other external components).
This makes business logic testable without any dependencies to other systems and gives the flexibility to make changes on the adapters easily. An example would be that one could swap out an Oracle or SQL server database with a Mongo or Cockroach database without affecting the business rules because they aren’t dependent on the database that is used.
Domain Object
The domain object represents the core domain model and is the core of the application. It can have state and business behavior. The domain object doesn’t have any dependency on the other components. When a business requirement changes, then the domain object is modified to reflect those changes.
Let’s create a domain object for the Account domain object:
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private String accountId;
private String name;
private String owner;
private BigInteger balance;
private Instant createdAt;
public void withdraw(final BigInteger money) {
this.setBalance(this.getBalance().subtract(money));
}
public void deposit(final BigInteger money) {
this.setBalance(this.getBalance().add(money));
}
}
In the Account object, you can see the fields and some behavior (deposit &
withdraw) of the Account object.
Use Cases
Use cases are the abstract definition of what the user would like to do in your application. Just like the domain objects, use cases are also a part of the application core and do not have any dependency on the other components. All the business logic, validations are happening in the use case classes.
In our account service example, I prefer create two different use case
interfaces, one of them is AccountUseCase and TransferMoneyUseCase to show
you an example the usage of loosely-coupled use case components.
Note:
UseCaseis used to show the structure, butServiceis preferred.
public interface AccountUseCase {
Account retrieveAccountById(final Long id);
List<Account> retrieveAccounts();
Account create(final @Valid CreateAccountCommand command);
void delete(final Long id);
Transfer transferMoney(@Valid final SendMoneyCommand command);
}
public interface TransferMoneyUseCase {
Transfer transferMoney(@Valid final SendMoneyCommand command);
}
When you check the implementation classes of use cases you will see how we use output ports to reach database layer.
Input and Output Ports
The application core uses dedicated interfaces called ports to communicate with the outside world. They allow the entry or exiting of data to and from the application.
An input port, “driving port”, allows the application core to expose the functionality to the outside world. An output port, “driven port”, is another type of interface