Dorian Frances

code enthusiast unraveling clean code magic!

How to bootstrap a Spring boot project using Clean Architecture

bootstrap
architecture

Introduction

Hello everyone ! Today we will talk about Spring and Java. More specifically, I’ll talk about how to bootstrap a spring boot project using Hexagonal Architecture. Spring boot is a very straightforward framework, but creating a very clean bootstrap using clean architecture can be disruptive.

Hexagonal Architecture bring to the fore many concepts like :

Many of you could think that using hexagonal architecture to start a little project can be a little bit wordy and you are not completely wrong but in the other hand, it also provides a comprehensive structure of your project.

πŸ‘‰ Also, I would not lie about the fact that I do not have enough knowledge about all the different kind of architecture. Here, I simply suggest a way of bootstrapping a project using hexagonal architecture. Feel free not to use it if you don’t want to. πŸ˜‰

Github repository

You can find the entire bootstrap project by clicking πŸ“‚ this link. Feel free to use it as you want !

Initialize a project

The first thing we need to do is to create our spring-boot project (I used maven here as my package manager).

Using IntelliJ IDEA, you might come up with something like this:

.
β”œβ”€β”€ pom.xml
└── src
    β”œβ”€β”€ main
    β”‚Β Β  β”œβ”€β”€ java
    β”‚Β Β  β”‚Β Β  └── org
    β”‚Β Β  β”‚Β Β      └── example
    β”‚Β Β  β”‚Β Β          └── Main.java
    β”‚Β Β  └── resources
    └── test
        └── java

Target project

The application we want to create here is pretty basic, we will build a RestAPI with one endpoint that will allow us to retrieve a user in a PostgreSQL database given its ID.

Going further, we pretty much see the elements that we will need:

Clean Architecture

For now, we see that our code structure does not fit hexagonal architecture. To solve this, we want our code to contain those elements:

πŸ‘‰ IMPORTANT: domain-driven-development state that our domain needs to be independent from all technical implementation. The fact is, technical implementation may change over time, your business code usually do not. However, our domain still needs to communicate with the other parts of our application, that is why, we will create application-domain and domain-infrastructure interfaces. by doing this, we ensure better modularity and better testability of our application (we’ll see this later in this article).

Now, our application should look like this:

.
β”œβ”€β”€ application
β”‚Β Β  β”œβ”€β”€ pom.xml
β”‚Β Β  └── src
β”‚Β Β      β”œβ”€β”€ main
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ java
β”‚Β Β      β”‚Β Β  β”‚Β Β  └── org.example
β”‚Β Β      β”‚Β Β  └── resources
β”‚Β Β      └── test
β”‚Β Β          └── java
β”œβ”€β”€ bootstrap
β”‚Β Β  β”œβ”€β”€ pom.xml
β”‚Β Β  └── src
β”‚Β Β      β”œβ”€β”€ main
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ java
β”‚Β Β      β”‚Β Β  β”‚Β Β  └── org
β”‚Β Β      β”‚Β Β  β”‚Β Β      └── example
β”‚Β Β      β”‚Β Β  β”‚Β Β          └── Main.java
β”‚Β Β      β”‚Β Β  └── resources
β”‚Β Β      └── test
β”‚Β Β          └── java
β”œβ”€β”€ domain
β”‚Β Β  β”œβ”€β”€ pom.xml
β”‚Β Β  └── src
β”‚Β Β      β”œβ”€β”€ main
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ java
β”‚Β Β      β”‚Β Β  β”‚Β Β  └── org
β”‚Β Β      β”‚Β Β  β”‚Β Β      └── example
β”‚Β Β      β”‚Β Β  └── resources
β”‚Β Β      └── test
β”‚Β Β          └── java
β”œβ”€β”€ infrastructure
β”‚Β Β  β”œβ”€β”€ pom.xml
β”‚Β Β  └── src
β”‚Β Β      β”œβ”€β”€ main
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ java
β”‚Β Β      β”‚Β Β  β”‚Β Β  └── org
β”‚Β Β      β”‚Β Β  β”‚Β Β      └── example
β”‚Β Β      β”‚Β Β  └── resources
β”‚Β Β      └── test
β”‚Β Β          └── java
└── pom.xml

We will now go through all the different folders to understand how we implemented each of them.

Deep diving in our project structure

Application

After that we implemented our rest-api-adapter, the folder has this structur:

application
β”œβ”€β”€ pom.xml
β”œβ”€β”€ rest-api-adapter
Β Β   β”œβ”€β”€ pom.xml
Β Β   β”œβ”€β”€ src
Β Β   β”œβ”€β”€ main
Β Β   β”‚Β Β  β”œβ”€β”€ java
Β Β   β”‚Β Β  β”‚Β Β  └── io.df.java.template.application.rest.api.adapter
Β Β   β”‚Β Β  β”‚Β Β                                  β”œβ”€β”€ controller
Β Β   β”‚Β Β  β”‚Β Β                                  β”‚Β Β  └── UserController.java
Β Β   β”‚Β Β  β”‚Β Β                                  └── mapper
Β Β   β”‚Β Β  β”‚Β Β                                      β”œβ”€β”€ DfExceptionMapper.java
Β Β   β”‚Β Β  β”‚Β Β                                      └── UserMapper.java
Β Β   β”‚Β Β  └── resources
Β Β   └── test
Β Β        └── java

The two main things that we’ll talk about here are our UserController and the UserMapper .

@RestController
@Tags(@Tag(name = "User"))
@AllArgsConstructor
public class UserController implements UserApi {

    final UserFacade userFacade;

    @Override
    public ResponseEntity<GetUserDTO> getUser(UUID id) {
        try {
            final User user = userFacade.getUser(id);
            return ResponseEntity.ok(getUserFromDomainToResponse(user));
        } catch (DfException exception) {
            return mapDfExceptionToContract(() -> errorToGetUserDTO(exception), exception);
        }

    }
}

This is the implementation of our user endpoint. We can see that this controller implements the UserApi. This, is an interface generated by the openapi-generator used in our contract-first methodology.

πŸ‘‰ If you look at the README.md file in the github repository, you will get an explanation about using contract-first methodology. Basically, it allows us to define (in a contract) the different endpoints of our API that will be generated during build so that we only need to implement them after.

Anyway, we notice different thins in this controller, the use of a UserFacade that we pass in the constructor of our controller (notice the @AllArgsConstructor decorator here). As I explained here, we pass an interface to our controller that our domain will then implement to increase modularity / testability of our code and independence of our domain.

The second thing is the use of getUserFromDomainToResponse method from the UserMapper class. We really want to make a distinction between domain models and API entity. That is the purpose of the mapper here.

πŸ‘‰ We’ll talk about the DfExceptionMapper class in the domain section.

Domain

Our domain looks like this:

domain
β”œβ”€β”€ pom.xml
β”œβ”€β”€ src
  Β Β  β”œβ”€β”€ main
  Β Β  β”‚Β Β  β”œβ”€β”€ java
  Β Β  β”‚Β Β  β”‚Β Β  └── io.df.java.template.domain
  Β Β  β”‚Β Β  β”‚Β Β                      β”œβ”€β”€ exception
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  β”œβ”€β”€ DfException.java
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  └── DfExceptionCode.java
  Β Β  β”‚Β Β  β”‚Β Β                      β”œβ”€β”€ model
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  └── User.java
  Β Β  β”‚Β Β  β”‚Β Β                      β”œβ”€β”€ port
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  β”œβ”€β”€ in
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  β”‚Β Β  └── UserFacade.java
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β  └── out
  Β Β  β”‚Β Β  β”‚Β Β                      β”‚Β Β      └── UserStoragePort.java
  Β Β  β”‚Β Β  β”‚Β Β                      └── service
  Β Β  β”‚Β Β  β”‚Β Β                          └── UserService.java
  Β Β  β”‚Β Β  └── resources
  Β Β  └── test
  Β Β      └── java
  Β Β  Β Β  Β Β  └── io.df.java.template.domain
  Β Β                              └── service
  Β Β                                  └── UserServiceTest.java

First, our UserService :

@AllArgsConstructor
public class UserService implements UserFacade {

    UserStoragePort userStoragePort;

    @Override
    public User getUser(UUID userId) throws DfException {
        final Optional<User> optionalUser = userStoragePort.getUser(userId);
        if (optionalUser.isEmpty()) {
            throw DfException.builder()
                    .dfExceptionCode(DfExceptionCode.USER_NOT_FOUND_EXCEPTION)
                    .errorMessage(String.format("Failed to find user with id %s", userId))
                    .build();
        }
        return optionalUser.get();
    }
}

This is the service that implements the interface called in the UserController . The same way, this service calls another interface to fetch the user that we want in the database.

All those interfaces are called port and you can see that they are defined in our domain wearing the designation port.in and port.out.

In the domain, we can also find our UserModel . This is the object that will be manipulated in our domain. In there we can not find entities from our API or our database. This would break hexagonal architecture principles.

Finally, we also see an exception folder. In this project, we defined our own exceptions allowing us to customize the exceptions that we wanna throw. For example, in our UserService if we threw an HttpNotFoundException , this would mean that our domain is aware that we are using an HTTP protocol as a technical implementation. Again, this breaks hexagonal architecture and domain-driven-development principles. To avoid that, we throw customized exceptions that we’ll map INSIDE of our rest-api-adapter (this is the purpose of the DfExceptionMapper class in our application folder).

This way, our domain only knows that he has to throw an exception without knowing how those are handled in the technical implementations.

Infrastructure

infrastructure
β”œβ”€β”€ pom.xml
β”œβ”€β”€ postgres-adapter
  Β Β  β”œβ”€β”€ pom.xml
  Β Β  β”œβ”€β”€ src
    Β Β  Β Β  β”œβ”€β”€ main
    Β Β  Β Β  β”‚Β Β  β”œβ”€β”€ java
    Β Β  Β Β  β”‚Β Β  β”‚Β Β  └── io.df.java.template.infrastructure
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                      └── postgres
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ adapter
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”‚Β Β  └── PostgresUserAdapter.java
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ configuration
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”‚Β Β  └── PostgresConfiguration.java
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ entity
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”‚Β Β  └── UserEntity.java
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ mapper
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          β”‚Β Β  └── UserEntityMapper.java
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                          └── repository
    Β Β  Β Β  β”‚Β Β  β”‚Β Β                              └── UserRepository.java
    Β Β  Β Β  β”‚Β Β  └── resources
    Β Β  Β Β  β”‚Β Β      └── db.changelog
    Β Β  Β Β  β”‚Β Β          β”œβ”€β”€ changelogs
    Β Β  Β Β  β”‚Β Β          β”‚ Β Β  └── 00000000_init_user_schema.sql
    Β Β  Β Β  β”‚Β Β          └── db.changelog-master.yaml
    Β Β  Β Β  └── test
    Β Β  Β Β     └── java

As mentioned earlier, this is where all technical implementations used by our application should be. Thus, in our case, we can only find our postgres-adapter designed to handle relation with our database.

In this project, we used Hibernate for our ORM along with Liquibase to handle database migration (I will not deep dive into the use of liquibase, but that explains the presence of the db.changelog directory in this adapter).

Again we can find a mapper directory to handle mapping between postgres entities and domain models.

Our PostgresUserAdapter class looks like that:

@AllArgsConstructor
public class PostgresUserAdapter implements UserStoragePort {

    private final UserRepository userRepository;

    @Override
    public Optional<User> getUser(UUID userId) {
        return userRepository.findById(userId).map(UserEntityMapper::fromEntityToDomain);
    }
}

And the configuration of the entire module is:

@Configuration
@EnableAutoConfiguration
@EnableTransactionManagement
@EnableJpaAuditing
@EntityScan(basePackages = {"io.df.java.template.infrastructure.postgres.entity"})
@EnableJpaRepositories(basePackages = {"io.df.java.template.infrastructure.postgres.repository"})
public class PostgresConfiguration {

    @Bean
    public PostgresUserAdapter postgresUserAdapter(final UserRepository userRepository) {
        return new PostgresUserAdapter(userRepository);
    }
}

We define here the packages where Spring will be able to find our Entities and JpaRepositories .

Bootstrap

Finally, the bootstrap folder. As we passed interfaces as dependencies along all our application, we now have to tell our application which classed should implement this or this interface. This is done during the runtime of our application through this bootstrap folder.

bootstrap
β”œβ”€β”€ pom.xml
β”œβ”€β”€ src
Β Β   β”œβ”€β”€ main
Β Β   β”‚Β Β  β”œβ”€β”€ java
Β Β   β”‚Β Β  β”‚Β Β  └── io.df.java.template.bootstrap
Β Β   β”‚Β Β  β”‚Β Β                      β”œβ”€β”€ DfJavaTemplateApplication.java
Β Β   β”‚Β Β  β”‚Β Β                      └── configuration
Β Β   β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ DfJavaTemplateConfiguration.java
Β Β   β”‚Β Β  β”‚Β Β                          β”œβ”€β”€ DomainConfiguration.java
Β Β   β”‚Β Β  β”‚Β Β                          └── RestApiConfiguration.java
Β Β   β”‚Β Β  └── resources
Β Β   β”‚Β Β      └── application.yaml
Β Β   └── test
Β Β       β”œβ”€β”€ java
Β Β       |Β Β  └── io.df.java.template.bootstrap
Β Β       β”‚Β Β                      β”œβ”€β”€ DfJavaTemplateITApplication.java
Β Β       β”‚Β Β                      └── it
Β Β       β”‚Β Β                          β”œβ”€β”€ AbstractDfJavaTemplateBackForFrontendApiIT.java
Β Β       β”‚Β Β                          └── UserApiIT.java
Β Β       └── resources
Β Β           └── application-it.yaml

We can see in the structure of this folder different configuration files. Indeed, we will find one configuration file for our domain folder and one for each adapters of our application (you can find the configuration folder of our postgres-adapter in the adapter itself).

Our DfJavaTemplateApplication class will then, looks like this:

@SpringBootApplication
@EnableConfigurationProperties
@Import(value = {DfJavaTemplateConfiguration.class, DomainConfiguration.class, PostgresConfiguration.class, RestApiConfiguration.class})
@Slf4j
@EnableAsync
@AllArgsConstructor
public class DfJavaTemplateApplication {
    public static void main(String[] args) {
        SpringApplication.run(DfJavaTemplateApplication.class, args);
    }
}

πŸ‘‰ Do not forget to import all of your application configurations in the startup class so that Spring can find them and initialize each of the defined beans.

For example, the dependency injection for our RestApiConfiguration is:

@Configuration
public class RestApiConfiguration {

    @Bean
    public UserController userController(final UserService userService) {
        return new UserController(userService);
    }
}

Testing strategy

Obviously, we can not forget to test our application. That is why, in this project, you will find to types of tests.

Unit Tests

An example of unit test is present in our domain folder. This type of tests are used to validate that our application behave as we want it to.

πŸ‘‰ For these tests, we do not want to reach technical implementations. Thus, we’ll have to mock every call to technical implementations. Fortunately, thanks to the use of interfaces, it is really easy to do.

For example:

@Test
    void should_return_user_given_a_user_id() throws DfException {
        // Given
        final UUID userIdFake = UUID.randomUUID();
        final String usernameFake = faker.ancient().god();

        // When
        when(userStoragePortMock.getUser(userIdFake))
                .thenReturn(Optional.of(User.builder().id(userIdFake).username(usernameFake).build()));
        final User resultUser = userService.getUser(userIdFake);

        // Then
        assertThat(resultUser.getId()).isEqualTo(userIdFake);
        assertThat(resultUser.getUsername()).isEqualTo(usernameFake);
    }

Integration tests

Those tests are different. They also have more value as they test the behaviour of our application WITH the use of technical implementations. Consequently, they are more difficult to write. Indeed, we need to fake the startup of our application in order to enable us to fake some calls on our API.

You’ll find an example of integration test file in the bootstrap folder.

Continuous Integration (CI)

We also integrated a .circleci folder along with a config.yml file in this project.

version: 2.1
machine: true

jobs:
  build-and-test:
    machine:
      image: ubuntu-2004:202107-02
      docker_layer_caching: true
    steps:
      - checkout
      - run:
          name: Install OpenJDK 17
          command: |
            sudo apt-get update && sudo apt-get install openjdk-17-jdk
            sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java
            sudo update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac
      - run:
          name: Generate cumulative pom.xml checksum
          command: |
            find . -type f -name "pom.xml" -exec sh -c "sha256sum {} >> ~/pom-checksum.tmp" \;
            sort -o ~/pom-checksum ~/pom-checksum.tmp
      - restore_cache:
          keys:
            - df-java-template-multi-module-mvn-
      - run:
          name: Build
          command: ./mvnw install -T 12 -DskipTests -DskipITs
      - save_cache:
          paths:
            - ~/.m2
          key: df-java-template-multi-module-mvn-
      - run:
          name: Unit Tests
          command: ./mvnw test -T 12
      - run:
          name: Integration Tests
          command: ./mvnw integration-test -T 12

workflows:
  build-test-deploy:
    jobs:
      - build-and-test

The important things to see here are:

Conclusion

Though this article, I explained how I used hexagonal architecture and domain-driven-development to create my spring-boot project since I started developing. Of course, I only have few years of experience, thus, I do not have much knowledge about other code architectures. However, I find this architecture really educational and made me understand many key concepts of application design.

Do not hesitate to give me your feedback about this and if you want to share your knowledge about everything, you are well welcome πŸ˜‰.