Hexagonal Architecture on Spring Boot

written in architecture, hexagonal, java, spring, spring-boot

In this article, I’ll show how to implement a Spring Boot application using Hexagonal Architecture.

We’ll build a Bank Account simulation with deposit and withdraw operations exposed through REST endpoints.

Hexagonal Architecture

Hexagonal architecture is an architectural style that focuses on keeping the business logic decoupled from external concerns.

The business core interacts with other components through ports and adapters. This way, we can change the underlying technologies without having to modify the application core.

Application Core

Domain Model

Let’s start with the domain model. Its main responsibility is to model the business rules. It also verifies that the objects are always in a valid state:

BankAccount.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BankAccount {

      private Long id;
      private BigDecimal balance;

      // Constructor

      public boolean withdraw(BigDecimal amount) {
          if(balance.compareTo(amount) < 0) {
              return false;
          }

          balance = balance.subtract(amount);
          return true;
      }

      public void deposit(BigDecimal amount) {
          balance = balance.add(amount);
      }

}

The domain model should have no dependency on any specific technology. That’s the reason why you’ll find no Spring annotations here.

Ports

Now it’s time to have our business logic interact with the outside world. To achieve this, we’ll introduce some ports.

First, let’s define 2 incoming ports. These are used by external components to call our application. In this case, we’ll have one per use case. One for Deposit:

DepositUseCase.java
1
2
3
public interface DepositUseCase {
      void deposit(Long id, BigDecimal amount);
}

And one for Withdraw:

WithdrawUseCase.java
1
2
3
public interface WithdrawUseCase {
      boolean withdraw(Long id, BigDecimal amount);
}

Similarly, we’ll also have 2 outgoing ports. These are for our application to interact with the database. Once again, we’ll have one per use case. One for Loading the Account:

LoadAccountPort.java
1
2
3
public interface LoadAccountPort {
      Optional<BankAccount> load(Long id);
}

And one for Saving it:

SaveAccountPort.java
1
2
3
public interface SaveAccountPort {
      void save(BankAccount bankAccount);
}

Service

Next, we’ll create a service to tie all the pieces together and drive the execution:

BankAccountService.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
public class BankAccountService implements DepositUseCase, WithdrawUseCase {

      private LoadAccountPort loadAccountPort;
      private SaveAccountPort saveAccountPort;

      // Constructor

      @Override
      public void deposit(Long id, BigDecimal amount) {
          BankAccount account = loadAccountPort.load(id)
                  .orElseThrow(NoSuchElementException::new);
  
          account.deposit(amount);

          saveAccountPort.save(account);
      }

      @Override
      public boolean withdraw(Long id, BigDecimal amount) {
          BankAccount account = loadAccountPort.load(id)
                  .orElseThrow(NoSuchElementException::new);

          boolean hasWithdrawn = account.withdraw(amount);
  
          if(hasWithdrawn) {
              saveAccountPort.save(account);
          }
          return hasWithdrawn;
      }
}

Note how the service implements the incoming ports. On each method, it uses the Load port to fetch the account from the database. Then, it performs the changes on the domain model. And finally, it saves those changes through the Save port.

Adapters

Web

To complete our application, we need to provide implementations for the defined ports. We call these adapters.

For the incoming interactions, we’ll create a REST controller:

BankAccountController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/account")
public class BankAccountController {

      private final DepositUseCase depositUseCase;
      private final WithdrawUseCase withdrawUseCase;

      // Constructor

      @PostMapping(value = "/{id}/deposit/{amount}")
      void deposit(@PathVariable final Long id, @PathVariable final BigDecimal amount) {
          depositUseCase.deposit(id, amount);
      }

      @PostMapping(value = "/{id}/withdraw/{amount}")
      void withdraw(@PathVariable final Long id, @PathVariable final BigDecimal amount) {
          withdrawUseCase.withdraw(id, amount);
      }
  }

The controller uses the defined ports to make calls to the application core.

Persistence

For the persistence layer, we’ll use Mongo DB through Spring Data:

SpringDataBankAccountRepository.java
1
public interface SpringDataBankAccountRepository extends MongoRepository<BankAccount, Long> { }

Also, we’ll create a BankAccountRepository class that connects the outgoing ports with the SpringDataBankAccountRepository:

BankAccountRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class BankAccountRepository implements LoadAccountPort, SaveAccountPort {

      private SpringDataBankAccountRepository repository;

      // Constructor

      @Override
      public Optional<BankAccount> load(Long id) {
          return repository.findById(id);
      }

      @Override
      public void save(BankAccount bankAccount) {
          repository.save(bankAccount);
      }
}

Infrastructure

Finally, we need to tell Spring to expose the BankAccountService as a bean, so it can be injected in the controller:

BeanConfiguration.java
1
2
3
4
5
6
7
8
9
@Configuration
@ComponentScan(basePackageClasses = HexagonalApplication.class)
public class BeanConfiguration {

      @Bean
      BankAccountService bankAccountService(BankAccountRepository repository) {
          return new BankAccountService(repository, repository);
      }
}

Defining the beans in the Adapters layer helps us maintain the infrastructure code decoupled from the business logic.

Conclusion

In this article, we’ve seen how to implement an application using Hexagonal Architecture and Spring Boot. This is what the system ends up looking like:

The code for this example is available on Github.


This article is based on the highly recommendable “Get Your Hands Dirty on Clean Architecture by Tom Hombergs, and this Baeldung article by Łukasz Ryś.


Comments