Hexagonal Architecture example

In this tutorial we will create simple Spring application based on the Hexagonal Architecture (also called as ports and adapters). This concept is focused on separating implementation of business rules from technical details, it is very flexible and doesn’t require specific implementation (like naming of the modules , classes, adapters and so on). You can use this example as template to start with your own application.

Technologies used:

  1. Spring Boot 2.1.0.RELEASE
  2. JDK 1.8
  3. Gradle 4.10.2

1. Project setup

We will organize our project into several modules:
  • pets-domain – contains business logic (domain data structures and business processing rules). This module should define and provide primary ports – used for calling business logic (like entry point for this module). All secondary ports, which domain should depend on, must also be defined here, but without the implementation. These ports are used to communicate with the world. Interface of all the ports should depend only on domain model.build.gradle for the module:
    dependencies {
      compile group: 'org.springframework.boot', name: 'spring-boot-starter-web'
    }
    
  • pets-infrastructure – contains implementation for secondary ports defined in pets-domain module. Implementations of these ports are called secondary adapters – they directly communicate with specific datasources (databases, filesystems, etc), they can also call other applications (REST, SOAP, JMS, etc). In simple words, this module should contain implementation details related with providing data (or with communicating with the world) for the the domain module.build.gradle for the module:
    dependencies {
      compile project(':pets-domain')
    }
  • pets-api – module containing primary adapters (which implement primary ports) – these module translates calls from other systems (other applications or just GUI) to call logic from our domain module.build.gradle for the module:
    dependencies {
      compile project(':pets-domain')
      compile group: 'org.springframework.boot', name: 'spring-boot-starter-web'
    
    }
  • pets-app – technical module. It wires all the previous modules and contains configuration for the application
    build.gradle for the module:

    apply plugin: 'application'
    mainClassName = 'com.devcases.pets.PetsApplication'
    dependencies {
      compile project(':pets-api')
      compile project(':pets-infrastructure')
      compile group: 'org.springframework.boot', name: 'spring-boot-starter-web'
    }
And here is the root gradle configuration file, gathering ale the dependencies:
plugins {
    id "base"
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
}

description = """pets"""

allprojects {
    group = 'com.devcases'
    version = '0.0.1-SNAPSHOT'

    apply plugin: 'java'

    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
    }

    repositories {
        maven { url "http://repo.maven.apache.org/maven2" }
        mavenCentral()
    }
}

subprojects {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        dependencies {
            dependency 'org.springframework.boot:spring-boot-starter-web:2.1.0.RELEASE'
        }
    }
}

2. Domain Logic

The most interesting module in this concept is the domain module – as mentioned above, it contains only raw business logic, it doesn’t even know about how it is deployed (any spring annotations, etc).

In our example we have one simple model:

package com.devcases.pets;

public class Pet {
    private String id;
    private String name;
    private String species;

    public Pet(String id, String name, String species) {
        this.id = id;
        this.name = name;
        this.species = species;
    }

    public void play() {
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getSpecies() {
        return species;
    }
}
Notice this model is immutable – it’s important, because it will be returned to other modules.
Here is primary port that should be called for obtaining our list of pets:
package com.devcases.pets;

import java.util.List;

public interface PetService {
  List getAllPets();
}
And implementation of this interface:
package com.devcases.pets;

import java.util.List;

public class PetServiceImpl implements PetService {

  private PetRepository repository;

  public PetServiceImpl(PetRepository repository) {
    this.repository = repository;
  }

  @Override
  public List<Pet> getAllPets() {
    //do some logic to retrieve all pets
    return repository.getAllPets();
  }
}
This class contains some business logic (very simple in our example). We also operate only on objects defined in domain module – we shouldn’t even know how are represented rest requests to our functionality or where the pets are persisted.
Its worth to notice, that there is no technical information about how it will be instantiated – as singleton, prototype, maybe pool of beans? – in this place we don’t bother about these technical things, we are only interested in business rules.
This approach also helps us to build better unit tests – without bothering about dependencies in the spring application contexts, thanks to instantiation by the constructor we also don’t need to use additional frameworks (like mockito) for mocking our dependencies – our tests will be faster.

We see, that the service use the interface PetRepository  to obtain pets – in our case this is the secondary port:

package com.devcases.pets;

import java.util.List;

public interface PetRepository {
  List getAllPets();
}

3. Infrastructure

This modules is dependent on pets-domain module, it contains implementation of secondary port:
package com.devcases.pets;

import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
public class PetRepositoryImpl implements PetRepository {

  @Override
  public List getAllPets() {
    //todo replace source of pets with any real datasource
    return new ArrayList<>(Arrays.asList(
        new Pet("1", "Daisy", "dog"),
        new Pet("2", "Bella", "cat")));
  }
}
For simplicity we create list of pets on the fly, but in real application they should be obtained form the real datasource – database (Oracle, PostgreSQL, etc), file, calling other service (for example REST or SOAP) or other. This is also the place where we should map model from other systems to the domain model.

4. Api module

This is module, which should contain technical information about calling our domain. In our case we have spring controller providing rest resource returning our pets:
package com.devcases.pets;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/pets")
public class PetController {

  @Autowired
  private PetService petService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public List getAllPets() {
    return petService.getAllPets();
  }
}
In this place we could map our domain model to specific response but form simplicity we skip this step.

5. App module

This is the place where all the modules are configured together:


package com.devcases.pets;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PetsApplication {

 public static void main(String[] args) {
  SpringApplication.run(PetsApplication.class, args);
 }
}

package com.devcases.pets.configuration;

import com.devcases.pets.PetRepository;
import com.devcases.pets.PetServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {

  @Bean
  public PetServiceImpl getUserService(PetRepository petRepository) {
    return new PetServiceImpl(petRepository);
  }
}
Here we decide how service from the domain module is deployed.After compiling, running the application, we can hit the url http://localhost:8080/pets/ and the result should be like that:

[
  {
    "id": "1",
    "name": "Daisy",
    "species": "dog"
  },
  {
    "id": "2",
    "name": "Bella",
    "species": "cat"
  }
]

The example code you can download from here.

Leave a Reply

Your email address will not be published. Required fields are marked *