A Simple Hexagonal Architecture Example Explained In A Nutshell

1. Overview

This is a quick hexagonal architecture example with Spring boot.

2. Hexagonal Architecture

This is a design pattern that ensures decoupling between the core domain logic inside the hexagon and its external dependencies. Mainly, the intent is to isolate the core logic and make it accessible to adapters through specific contracts. Likewise, this is also the main intent of an n-tier architecture but in this case, the tiers tend to have many implementation details of the tier below.

The main concept is very similar to Dependency Injection (DI)  where high-level modules should be independent of low-level modules.  One way to implement this architecture is through the use of an Inversion of Control (IOC) container.

The benefits of this architecture are:

  • the core layer is independent and fully functional on its own
  • implementations of the interfaces are replaceable which makes it great to test.
  • development of the external layers will not disrupt the functionality of the inner core layer

3. Core Domain

Let us introduce this hexagonal architecture example by creating a simple user service to store and fetch users.

First, let’s start by creating a simple User object.

public class User {

  private long id;
  private String name;

  public User() {
  }

  public User(long id, String name) {
       super();
       this.id = id;
       this.name = name;
  }

  // getters and setters
}Code language: Java (java)

Secondly, we create a UserService interface to add and retrieve users.

public interface UserService {

    void addUser(User user);

    List<User> getUsers();
}Code language: Java (java)

Next, let’s create a UserDTO object so that it is used outside the core domain.

public class UserDTO {

    private long id;
    private String name;

    // getters and setters
}Code language: Java (java)

We also need to create an interface UserDataAdapter for user data related calls. This will act as a contract between the core domain and the data adapter layer. In this way, we abstract the data repository from our core domain. As a result, any changes to the data layer will not break our service class UserServiceImpl.

public interface UserDataAdapter {

     void addUser(UserDTO user);
  
     List<UserDTO> getUsers();
}Code language: Java (java)
@Service
public class UserServiceImpl implements UserService {

  @Autowired
  private UserDataAdapter userDataAdapter;

  @Autowired
  private CoreModelMapper coreModelMapper;

  public void addUser(User user) { 
     userDataAdapter.addUser(coreModelMapper.map(user, UserDTO.class));
  }

  public List<User> getUsers() {
     return coreModelMapper.mapAsList(userDataAdapter.getUsers(), User.class);
  }
}Code language: JavaScript (javascript)

The UserServiceImpl class used  Orika to map between the User object and UserDTO.

4. Primary Port and Adapters

primary port is an external service that starts the interaction with our application. Usually, one or more primary adapters work on top of this port. The adapters use a specific contract to forward messages sent by the port to the core domain.

So, let’s create a REST Controller to be our primary adapter. The controller will forward any messages received to the core domain via the UserService bean.

@RestController
public class UserController {

  @Autowired
  private UserService userService;

  @PostMapping("/users")
  @ResponseStatus(HttpStatus.CREATED)
  public void addUser(@RequestBody User user) {
       userService.addUser(user);
  }

  @GetMapping("/users")
  public List<User> getUsers() {
       return userService.getUsers();
  }
}Code language: Java (java)

5. Secondary Port and Adapter

Sometimes our application needs to talk to other external services also called secondary ports. For example, our application will need to talk to a database (secondary port). In our case, an embedded H2 database will be used.

@Configuration
public class PersistenceConfig {

  @Bean
  public DataSource dataSource() {
       EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
       EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2)
       .addScript("mySchema.sql")
       .addScript("myData.sql")
       .build();
       return db;
  }
}Code language: PHP (php)

Let the UserDataAdapterImpl class be our secondary adapter. Briefly, it creates and retrieves users by calling the save() and findAll()  methods of the JPARepository interface.

@Component
public class UserDataAdapterImpl implements UserDataAdapter {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private ModelMapper modelMapper;

  public void addUser(UserDTO user) {
       userRepository.save(modelMapper.map(user, UserEntity.class));
  }

  public List<UserDTO> getUsers() {
       return modelMapper.mapAsList(userRepository.findAll(), UserDTO.class);
  }
}
Code language: JavaScript (javascript)
public interface UserRepository extends JpaRepository<UserEntity, Long> {

}Code language: PHP (php)

6. Conclusion

In conclusion, this pattern is ideal in project setups with many developers working simultaneously. However, the main advantage of using a Hexagonal Architecture is that the code in the core domain is kept protected from any changes to the ports or adapters.

Scroll to Top