Hexagonal Architecture in Java

java

4 februari 2020 – this is a special guest article by Eonics consultant João Esperancinha. To reach the widest possible audience this article is written in English instead of Dutch. Enjoy this technical deep-dive into Hexagonal Architecture in Java, the complete project can also be referenced via this Gitlab repository.

Introduction

The hexagonal architecture principle was created by Alistair Cockburn in 2005. This is one of the many forms of DDD (Domain Driven Design Architecture). The goal was to find a way to solve or otherwise mitigate general caveats introduced by object oriented programming. It is also known as Ports and Adapters architecture. The hexagon concept isn’t related to a six side architecture nor does it have anything to do with the geometrical form. A hexagon has six sides indeed, but the idea is to illustrate concept of many ports. This shape is also easier to split into two and to be used as a representation of the business logic of the application. The idea is to separate the application we want to develop into three separate parts. The left, the core and the right. From an even broader concept we want to differentiate the concepts of inside and outside. Inside is the business logic and the application itself and outside is whatever we are using to connect and interact with the application.

Core

The core of an application can be defined as the place where the business logic of the application happens. An application core receives data, performs operations on it and optionally may communicate with other external components like databases or persistence entities.

Ports

Ports represent the boundaries of the application. Frequently they are implemented as interfaces to be used by outside parties. Their implementation resides outside the application, although they share the same domain.

Primary ports are also known as driving ports. These are the first communication points between the outside and the application core. They are also known as Inbound Ports. These ports “drive” the application. This means that this is where requests get through to the application. The upstream in this case contains data and the downstream contains the response to that request. Primary ports reside on the left side of the hexagon.

Secondary ports are in contrast known as driven ports. These also live on the outside and symmetrically to the primary ports, on the right side of the hexagon. The application core uses secondary ports to upstream data to external entities. For example, an operation that needs data from the database will use a secondary port. The application “drives” the port in order to get data. The downstream will contain the data coming from external entities on the right. Because the application sends data to the outside, secondary ports also get called Outbound Ports.

Adapters

Primary adapters are implementations of primary ports. These are completely independent of the application core. This presents one of the clear advantages of this architecture. By implementing a port on the outside, we have control over how the implementation is done. This means that we can freely implement different forms of getting the data through to the application, without affecting the application itself. Just as ports primary adapters can also be called driving adapters. Examples of this are REST services and GUI’s.

Secondary adapters are implementations of secondary ports. Just as primary adapters, these are also independent of the application core with the same clear advantage. More often, we find that it’s in the secondary ports that lie the more difficult questions regarding the choice of technology. Frequently there is always the question on how do we actually want to implement a persistence layer. It can be difficult to choose the right database, file system or something else. By using adapters, we can easily change adapters as we please. This means that regardless of the implementation, our application also does not change.  It will only know the operations it needs to call and has no idea of how they are implemented. In the same way as primary adapters, secondary adapters are also referred as driven adapters.

Implementation

The application we will build as an example manages a song lyric storage system. It stores the related artist and a lyrics text. We can then access an endpoint which will randomly show a certain lyric and the related artist. We can also perform all other POST, PUT, DELETE and GET operators to perform CRUD (Create, Read, Update, Delete) operations via JPA (Java Persistence API) repositories. I purposely made this application simple while still including all common operations. This will help in understanding the important concepts like core, domain and infrastructure within this architecture.

Hexagonal Architecture diagram

The hexagonal architecture for the lyrics application

Structure

In the previous points I’ve mentioned a few keywords that are important for setting up our application. Since this is a demo application, a few considerations are important. I want the application to be simple, but also to represent what most applications do at it’s core. Most applications have a persistence framework, a business model and a presentation layer.

In this example we will use Spring in order to use the MVC (Model View Controller) pattern. To run the application we will use use Spring Boot. To access our database we use JPA repositories and finally we will use an H2 in memory database. You will also see in that I’m using JUnit Jupiter, Mockito and AssertJ. These are out off scope for this tutorial, but you can follow the links if you want to learn more about those technologies.

The pom.xml for this project will looks as follows:



    4.0.0
    pom
    
        favourite-lyrics-domain
        favourite-lyrics-core
        favourite-lyrics-jpa
        favourite-lyrics-starter
        favourite-lyrics-test
        favourite-lyrics-rest
    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.2.RELEASE
        
    
    org.jesperancinha.lyrics
    favourite-lyrics
    0.0.1-SNAPSHOT
    favourite-lyrics
    Favourite Lyrics App

    
        13
        1.4.200
        1.18.10
        5.2.2.RELEASE
    

    
        
            
                org.jesperancinha.lyrics
                favourite-lyrics-domain
                ${project.version}
            
            
                org.jesperancinha.lyrics
                favourite-lyrics-core
                ${project.version}
            
            
                org.jesperancinha.lyrics
                favourite-lyrics-rest
                ${project.version}
            
            
                org.jesperancinha.lyrics
                favourite-lyrics-jpa
                ${project.version}
            
            
                com.h2database
                h2
                ${h2.version}
                runtime
            
            
                org.springframework
                spring-tx
                ${spring-tx.version}
            
            
                org.projectlombok
                lombok
                ${lombok.version}
                true
            
        
    

Domain

Let’s have a look at what we need as a domain. Domain is in this case is anything that we can share between the core of the application and the ports.

The first thing to do is to define how we want data to be transferred around. In our case we do this via a DTO (Data Transfer Object):

@AllArgsConstructor
@Builder
@Data
@NoArgsConstructor
public class LyricsDto {

    private String lyrics;

    private String participatingArtist;

}

Normally you may need an exception that can be propagated throughout your architecture. This is a valid, but also very simplistic approach. Further discussion on this would require a new article and it goes off the scope of this article:

public class LyricsNotFoundException extends RuntimeException {

    public LyricsNotFoundException(Long id) {
        super("Lyrics with id %s not found!".formatted(id));
    }
}

Furthermore, this is where you create your outbound port. In our case and at this point we know we want persistence, but we are not interested in how it’s implemented. This is why we only create an interface at this point.

public interface LyricsPersistencePort {

    void addLyrics(LyricsDto lyricsDto);

    void removeLyrics(LyricsDto lyricsDto);

    void updateLyrics(LyricsDto lyricsDto);

    List getAllLyrics();

    LyricsDto getLyricsById(Long lyricsId);
}

Notice that our interface declares all necessary CRUD methods.

2.3. Core

Core works hand in hand with Domain. Both of them could be incorporated into one single module. However, this separation is very important because it makes core an implementation of only the business logic.

Core is where we find our service interface:

@Service
public class LyricsServiceImpl implements LyricsService {

    private final LyricsPersistencePort lyricsPersistencePort;

    public LyricsServiceImpl(LyricsPersistencePort lyricsPersistencePort) {
        this.lyricsPersistencePort = lyricsPersistencePort;
    }

    @Override
    public void addLyrics(LyricsDto lyricsDto) {
        lyricsPersistencePort.addLyrics(lyricsDto);
    }

    @Override
    @Transactional
    public void removeLyrics(LyricsDto lyricsDto) {
        lyricsPersistencePort.removeLyrics(lyricsDto);
    }

    @Override
    public void updateLyrics(LyricsDto lyricsDto) {
        lyricsPersistencePort.updateLyrics(lyricsDto);
    }

    @Override
    public List getAllLyrics() {
        return lyricsPersistencePort.getAllLyrics();
    }

    @Override
    public LyricsDto getLyricsById(Long lyricsId) {
        return lyricsPersistencePort.getLyricsById(lyricsId);
    }
}

Changes in core also represent changes in the business logic and this is why, for small applications there isn’t really a reason to further separate the port and the adapter into different modules. However, increased complexity may lead to splitting up the core into other different cores with different responsibilities. Take note that external modules should only use the interface and not the implementation of it.

JPA

First, we should create an entity that reflects the data we want to save. In this case we need to think about the participatingArtist and the lyrics themselves. Because it’s an entity, it also needs its ID. Please note that Artist is a field that could be set into another entity. I’m not doing that in this example because it would add further complexity and another ER (entity relationship) database paradigm, which is out off scope:

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "LYRICS")
@Data
public class LyricsEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column
    private Long lyricsId;

    @Column
    private String lyrics;

    @Column
    private String participatingArtist;

}

Now we can implement our JPA repository implementation. It is our outbound port. This is where our CRUD lives:

public interface LyricsRepository extends JpaRepository {

    void deleteAllByParticipatingArtist(String name);

    LyricsEntity findByParticipatingArtist(String Name);

    LyricsEntity findByLyrics(String Lyrics);
}

Finally, we can implement our port. This is a step between core and the JPA repository. This is our adapter and it’s the implementation of how we want to access our JPA repository:

@Service
public class LyricsJpaAdapter implements LyricsPersistencePort {

    private LyricsRepository lyricsRepository;

    public LyricsJpaAdapter(LyricsRepository lyricsRepository) {
        this.lyricsRepository = lyricsRepository;
    }

    @Override
    public void addLyrics(LyricsDto lyricsDto) {
        final LyricsEntity lyricsEntity = getLyricsEntity(lyricsDto);
        lyricsRepository.save(lyricsEntity);
    }

    @Override
    public void removeLyrics(LyricsDto lyricsDto) {
        lyricsRepository.deleteAllByParticipatingArtist(lyricsDto.getParticipatingArtist());
    }

    @Override
    public void updateLyrics(LyricsDto lyricsDto) {
        final LyricsEntity byParticipatingArtist = lyricsRepository.findByParticipatingArtist(lyricsDto.getParticipatingArtist());
        if (Objects.nonNull(byParticipatingArtist)) {
            byParticipatingArtist.setLyrics(lyricsDto.getLyrics());
            lyricsRepository.save(byParticipatingArtist);
        } else {
            final LyricsEntity byLyrics = lyricsRepository.findByLyrics(lyricsDto.getLyrics());
            if (Objects.nonNull(byLyrics)) {
                byLyrics.setParticipatingArtist(lyricsDto.getParticipatingArtist());
                lyricsRepository.save(byLyrics);
            }
        }
    }

    @Override
    public List getAllLyrics() {
        return lyricsRepository.findAll()
                .stream()
                .map(this::getLyrics)
                .collect(Collectors.toList());
    }

    @SneakyThrows
    @Override
    public LyricsDto getLyricsById(Long lyricsId) {
        return getLyrics(lyricsRepository.findById(lyricsId)
                .orElseThrow((Supplier) () -> new LyricsNotFoundException(lyricsId)));
    }

    private LyricsEntity getLyricsEntity(LyricsDto lyricsDto) {
        return LyricsEntity.builder()
                .participatingArtist(lyricsDto.getParticipatingArtist())
                .lyrics(lyricsDto.getLyrics())
                .build();
    }

    private LyricsDto getLyrics(LyricsEntity lyricsEntity) {
        return LyricsDto.builder()
                .participatingArtist(lyricsEntity.getParticipatingArtist())
                .lyrics(lyricsEntity.getLyrics())
                .build();
    }

}

This completes our application implementation on the right side. Note that I’ve implemented the update operation very simplistically. If the coming DTO already has a parallel via the participatingArtist then update the lyrics. If the coming DTO already has a parallel via the lyrics then update the participatingArtist. Also notice the getLyricsById method. It will throw the domain defined LyricsNotFoundException if the lyrics with the specified ID do not exist. All mechanisms are in place to access the database. Next we are going to see the implementation of a REST service which uses the inbound port to upstream data to the application.

REST

We use the typical way to implement a rest service using the Spring MVC framework. Essentially all we need is an interface to define what we need in our requests. This is our inbound port:

public interface LyricsController {

    @PostMapping("/lyrics")
    ResponseEntity addLyrics(@RequestBody LyricsDto lyricsDto);

    @DeleteMapping("/lyrics")
    ResponseEntity removeLyrics(@RequestBody LyricsDto lyricsDto);

    @PutMapping("/lyrics")
    ResponseEntity updateLyrics(@RequestBody LyricsDto lyricsDto);

    @GetMapping("/lyrics/{lyricsId}")
    ResponseEntity getLyricsById(@PathVariable Long lyricsId);

    @GetMapping("/lyrics")
    ResponseEntity> getLyrics();

    @GetMapping("/lyrics/random")
    ResponseEntity getRandomLyric();

}

And finally its implementation:

@Slf4j
@RestController
public class LyricsControllerImpl implements LyricsController {

    private final LyricsService lyricsService;

    private final Random random = new Random();

    public LyricsControllerImpl(LyricsService lyricsService) {
        this.lyricsService = lyricsService;
    }

    @Override
    public ResponseEntity addLyrics(LyricsDto lyricsDto) {
        lyricsService.addLyrics(lyricsDto);
        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    @Override
    public ResponseEntity removeLyrics(LyricsDto lyricsDto) {
        lyricsService.removeLyrics(lyricsDto);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @Override
    public ResponseEntity updateLyrics(LyricsDto lyricsDto) {
        lyricsService.updateLyrics(lyricsDto);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @Override
    public ResponseEntity getLyricsById(Long lyricsId) {
        try {
            return new ResponseEntity<>(lyricsService.getLyricsById(lyricsId), HttpStatus.OK);
        } catch (LyricsNotFoundException ex) {
            log.error("Error!", ex);
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @Override
    public ResponseEntity> getLyrics() {
        return new ResponseEntity<>(lyricsService.getAllLyrics(), HttpStatus.OK);
    }

    @Override
    public ResponseEntity getRandomLyric() {
        final List allLyrics = lyricsService.getAllLyrics();
        final int size = allLyrics.size();
        return new ResponseEntity<>(allLyrics.get(random.nextInt(size)), HttpStatus.OK);
    }
}

This results in  a complete rest service with which we can create lyrics, update lyrics, delete lyrics and read lyrics. We can do the latter in three different ways. We can read all of them, get one by id or just get one randomly. Application wise we have all the components in place. What we still miss at this point is the Spring environment and the Spring Boot Launcher. Let’s have a look at this next.

Spring Boot

Our application needs a launcher to get started. This is accomplished with Spring Boot:

@SpringBootApplication
@EnableTransactionManagement
public class LyricsDemoApplicationLauncher {
    public static void main(String[] args) {
        SpringApplication.run(LyricsDemoApplicationLauncher.class);
    }
}

Then we need to configure our environment and make sure that Spring Boot is aware of H2 and the JPA environment. You do this in the application.properties file:

# h2
spring.h2.console.path=/spring-h2-favourite-lyrics-console
spring.h2.console.enabled=true
# datasource
spring.datasource.url=jdbc:h2:file:~/spring-datasource-favourite-lyrics-url
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
# hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

Luckily for us spring will look for the schema file with the name schema.sql. So let’s create our very basic schema:

drop table if exists LYRICS;

create table LYRICS
(
    LYRICS_ID            bigint auto_increment primary key not null,
    PARTICIPATING_ARTIST VARCHAR(100)                      NOT NULL,
    LYRICS               VARCHAR(100)                      NOT NULL
);

Spring also looks for data.sql, so let’s put in some data:

insert into LYRICS (PARTICIPATING_ARTIST, LYRICS) values ('William Orbit', 'Sky fits heaven so fly it');
insert into LYRICS (PARTICIPATING_ARTIST, LYRICS) values ('Ava Max', 'Baby I''m torn');
insert into LYRICS (PARTICIPATING_ARTIST, LYRICS) values ('Faun', 'Wenn wir uns wiedersehen');
insert into LYRICS (PARTICIPATING_ARTIST, LYRICS) values ('Abel', 'Het is al lang verleden tijd');
insert into LYRICS (PARTICIPATING_ARTIST, LYRICS) values ('Billie Eilish', 'Chest always so puffed guy');

We are now ready. The only thing left is starting the application and making tests. All methods should be easily testable via curl or postman. As an example you can use curl to get a random lyric:

$ curl localhost:8080/lyrics/random 
{"lyrics":"Chest always so puffed guy","participatingArtist":"Billie Eilish"}

$ curl -d '{ "name":"wosssda", "volumes":10, "cashValue": 123.2, "genre": "woo"}' -H "Content-Type: application/json" -X POST http://localhost:8080/video-series

Conclusion

In this tutorial we have implemented a simple application according to the hexagonal architecture principles. Note that you can also implement this same architecture with other languages. You can find original examples in C#, Python, Ruby and many other languages in the literature. In the Java landscape you could also do this with other EE frameworks like JavaEE, JakartaEE or any other enterprise framework. The point is to always remember to isolate the inside from the outside and make sure that the communication is done via ports and that the implementation via adapters remains independent of the application core.

I have implemented this application with different modules which represented different responsibilities. However, you may also implement this in a single module. Once you try this you will see that the principles don’t really change. The only difference is that separate modules allow you to make changes independently and allows you to create different versions of your modules. However, both ways respect and follow this architecture, because in the end the point is to make interfaces and to use them in the data streaming core instead of their implementations. They are interchangeable without affecting the inside also known as application core.

All the source code of this application can be found on GitLab. Please feel free to reach out to me in case you have any further questions or comments.