loading...

Hexagonal Architecture / Ports and Adapters

jofisaes profile image João Esperancinha Updated on ・11 min read

Introduction

This architecture principle was created by [1] 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. This 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 essentially three separate parts. The left, the core and the right. Going into 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 parties 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

Primary ports are also known as driving ports. These are the first communication points between the outside and the application core. Therefore, they can still be otherwise 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 reisponse to that request. Primary ports reside on the left side of the hexagon.

Secondary ports

Secondary ports are in contrast known as driven ports. These also live on the outside and symmetrically to the primary ports they live 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 ports. The application “drives” the port in order to get data. The downstream contains thus 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

Adapters are essentially the implementation of ports. They are not supposed to be called directly in any point in code

Primary adapters

Primary adapters are implementations of primary ports. These are completely independent of the application core. This presents one of the more 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

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, or a file system or anything else. By using adapters, we can easily interchange 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

This application manages a lyric’s 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 operations to perform CRUD(Create, Read, Update, Delete) operations via JPA(Java Persistence API) repositories. I purposely made this application simple and with the common operations. It is important to understand concepts like core, domain and infrastructure and this is also the reason why I created this application with all of these operations.

Alt Text

Structure

In the previous points I’ve mentioned a few keywords that are important in order to set 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 I have chosen Spring in order to use the MVC(Model View Controller) pattern. If you want to know more about the MVC [2]pattern please follow the links below in the references section. To run the application we are going to use Spring Boot. To access our database I am using JPA repositories and finally I have chosen an H2 in memory database. You will also see in that I’m using JUnit Jupiter [3], Mockito [4] and AssertJ [5]. These are off the scope of these tutorial, but if you are interested in learning more about these frameworks, then please follow the links below in the references section.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>favourite-lyrics-domain</module>
        <module>favourite-lyrics-core</module>
        <module>favourite-lyrics-jpa</module>
        <module>favourite-lyrics-starter</module>
        <module>favourite-lyrics-test</module>
        <module>favourite-lyrics-rest</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>org.jesperancinha.lyrics</groupId>
    <artifactId>favourite-lyrics</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>favourite-lyrics</name>
    <description>Favourite Lyrics App</description>

    <properties>
        <java.version>13</java.version>
        <h2.version>1.4.200</h2.version>
        <lombok.version>1.18.10</lombok.version>
        <spring-tx.version>5.2.2.RELEASE</spring-tx.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jesperancinha.lyrics</groupId>
                <artifactId>favourite-lyrics-domain</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.jesperancinha.lyrics</groupId>
                <artifactId>favourite-lyrics-core</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.jesperancinha.lyrics</groupId>
                <artifactId>favourite-lyrics-rest</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.jesperancinha.lyrics</groupId>
                <artifactId>favourite-lyrics-jpa</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.h2database</groupId>
                <artifactId>h2</artifactId>
                <version>${h2.version}</version>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
                <version>${spring-tx.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

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<LyricsDto> getAllLyrics();

    LyricsDto getLyricsById(Long lyricsId);
}

Notice that our interface declares all necessary CRUD methods.

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:

public interface LyricsService {

    void addLyrics(LyricsDto lyricsDto);

    void removeLyrics(LyricsDto lyricsDto);

    void updateLyrics(LyricsDto lyricsDto);

    List<LyricsDto> getAllLyrics();

    LyricsDto getLyricsById(Long lyricsId);

}

And its implementation:

@Service
public class LyricsServiceImpl implements LyricsService {

    private final LyricsPersistence 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<LyricsDto> 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

Let’s create an entity. 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 off the 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. This is also known as our outboud port. This is where our CRUD lives:

public interface LyricsRepository extends JpaRepository<LyricsEntity, Long> {

    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<LyricsDto> 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<Throwable>) () -> 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

Iused the typical way to implement a rest service using the Spring MVC framework. Essentially all we need is firstly an interface to define what we need in our requests this is our inbout port:

public interface LyricsController {

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

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

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

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

    @GetMapping("/lyrics")
    ResponseEntity<List<LyricsDto>> getLyrics();

    @GetMapping("/lyrics/random")
    ResponseEntity<LyricsDto> getRandomLyric();

}

And finally its implementation:

@Slf4j
@RestController
public class LyricsControllerImpl implements LyricsController {

    private final LyricsServicePort lyricsServicePort;

    private final Random random = new Random();

    public LyricsControllerImpl(LyricsServicePort lyricsServicePort) {
        this.lyricsServicePort = lyricsServicePort;
    }

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

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

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

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

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

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

Here we have a complete typical rest service implemented where 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 everything ready. What we still don’t have 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. Now it’s all about 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”}

Conclusion

This has been an exercise to mostly understand the hexagonal architecture principles. Using Spring is just one possibility. You can implement this architecture with other languages. You can find original examples in C#, Python, Ruby and many other languages. In the Java landscape you can do this also with any EE framework 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 clear via ports and 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. You can try that and 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. In one single module you will have to make releases of everything at the same time as a follow up to code changes. 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 instead of their implementations. Their implementations will be used underwater, and they are interchangeable without affecting the inside also known as application core.

I have placed all the source code of this application in GitLab.

Thank you for reading!

Discussion

markdown guide
 

Hi!

I have a question, how the external world know about the lyrics Id? Since the lyricDto doesn't have the property Id?

I'm not familiar with Java, so maybe I've missed something :)

Great article!

 

Hi there!

Thanks for your comment! The id property is another paradigm that isn't strictly related with this architecture. You could add an Id, but I wouldn't add directly the one from the database. Maybe you could obfuscate it with some other technique. In any case, whether the id is present in de Dto or not, or if it should be there or not, is an independent architectural decision. Some projects use the id directly, but doing so, they are exposing the id to the outside world. Even if everything is protected and shielded against attacks, now everyone would know at least the ID of your objects. That could be a big step for hackers.

Hope this helps,
Cheers!

 

Thank you for your article!

I have a couple questions.

Why is the persistence port is injected to the service adapter? Is that the right place to call the persistence port?

And another silly question: where are adapters instantiated in your REST application? I see how it's done in the test application, but can't see in the REST one.

Thank you!

Yuriy

 

Hi there,

Thank you for your question.

All the business logic lives in the service. In this architecture type, that is where things like, calculations, parsing, transformations is made. Things like accessing databases or external parties, aren't made directly. This is where the concept of ports comes in. This is why the repository interface (a.k.a. port) is injected into the the service. This is to make sure that we can use repositories without any knowledge of its implementation. In other words, to be able to use them as a black box.

The adapters are the implementation of ports or in the Java world. We can say that ports are interfaces and adapters are implementations in java. They are not instantiated by the code of the REST application itself. Instead they are seamlessly instantiated by the library code. This is done by stereotyping our services, repositories and controllers by the use of the annotations @Service, @Repository and @RestController, respectively. If you look at the service constructor, you'll see just a normal constructor and this may lead to wonder where does the Repository comes from. That is where the stereotyping "magic" of Spring comes into action. Since we've stereotyped both the service and the repository, Spring then knows where and how to get them and we don't need to worry about it. Its important to note that by default and for this implementation, these are all singleton instances.

All of the concepts I've described above are part of IoC (Inversion of control) where very simply said it only means that we are giving Spring the control over the injection or creating the new instances. We don't specify that ourselves in the code. And that bit about the constructor is one example of DI (Dependency Injection). This is where we see that we do not specify the dependency implementation. This is passed through the constructor as a black box.

Please let me know if you have any more questions,

Cheers!

João

 

Thank you!

It's a little bit clearer now.

I'll continue learning this architecture, since I just started.

 

Hi João!

Congratulations for your article, it is very well written.

I believe it was the best article about hexagonal architecture with java spring that I have read so far, again, congratulations : -)

[]s

 

Hey Danilo!

Thank you so much for your comment! Happy to see that you enjoyed it so much!

Cheers!

 

Hi Juan,

First of all, congratulations for your great article. I just read it and kudos to you for that. I don’t think I’m saying anything different than you though. I appreciate your comment, but I’m afraid maybe this is just a communication issue and semantics. I’ve studied several examples of this and using interfaces is just one of them. Thank you for your comment and I’m glad to see that we don’t disagree on this.

Another thing that would help, if you want to explain your point clearly, is to provide your own implementation and then we can compare our notes. My example is also a Java version of what AC has done in his own presentation. I have put the links on my repo to his video on Youtube. I still don’t see any difference between what I did in Java and what AC did in C#. There are many implementations like this on the web anyways and so this makes it difficult to understand your point exactly. This why I think this may only be about semantics and that maybe you are only expecting me to say a certain order of words.

Thank you anyways!
Cheers!
João

 

Hello John
Very good article! Congratulations!

 

Hi Fernando!

Thank you so much! Appreciated man! Glad you like it!

Cheers!

Allright! that you mean!

I knew this was about semantics. I just knew it and I was right! . I really appreciate your comments man and I will correct that naming. Yes the naming is misleading, although the implementation still stands. I’m on it! Thanks again! You’re the best! 🥇👍😉🦄

 

Thanks for the article. It helped me to understand better the hexagon architecture. I'm looking forward to try my own implementation of it soon.