Hexagonal Architecture with QuarkusHexagonal Architecture with Quarkus
HappyCoders Glasses

Hexagonal Architecture With Quarkus
[Tutorial]

Sven Woltmann
Sven Woltmann
Last update: November 29, 2024

In this article, I’ll show you how we migrate the demo application we implemented in the previous two parts of this tutorial series using the hexagonal architecture to a Quarkus application – all without changing a single line of code in the application core.

What Happened So Far

In part two of this series of articles, we implemented a Java application following hexagonal architecture. In part three, we extended it with an additional database adapter to persist the data in a relational database instead of keeping it volatile in memory.

So far, we have not used an application framework. Instead, we implemented a simple dependency injection mechanism and directly connected all APIs like Jakarta Persistence and Jakarta RESTful Web Services and their implementations like Hibernate and RESTEasy. This required some boilerplate code (e.g., for bootstrapping and transaction management).

The following graphic shows the architecture of our application:

Hexagonal architecture of the sample application

If you want to recall the basics of hexagonal architecture, you can do so in the first part of the series just linked.

You can find the result of the work so far in the main branch of this GitHub repository.

What Is This Part About?

In this fourth part, we will replace the individual dependencies to the libraries with an application framework and remove some boilerplate code.

We use Quarkus in this part because Quarkus also relies on standard libraries like Jakarta RESTful Web Services instead of custom implementations, as the Spring framework does. That will simplify the migration. We will cover Spring in the next part of this series.

Also, in this part, as in the previous part, we will not have to change a single line of code in the application core – that is, in the model and application modules.

We will proceed step by step, and the application will be ready to run after each step:

  1. First, we will insert the Quarkus dependencies and remove the direct dependencies to those libraries brought by Quarkus.
  2. Then we will adapt the adapter module to Quarkus by using the dependency injection mechanism of Quarkus instead of injecting the dependencies manually.
  3. In the third step, we will do the same for the bootstrap module.
  4. And in the last step, we will convert the self-implemented transaction management and access to the database through the @Transactional annotation and Panache repositories.

Let’s start with step 1...

Step 1: Replacing the Dependencies

In the first step, we will adjust the dependencies defined in the pom.xml files without changing anything in the Java code. This way, our application remains executable without the application framework for the time being but already has a tidy set of dependencies.

Adapting the Parent pom.xml

We first define the Quarkus version in the <properties> block of the parent-pom.xml file since we will need it later in two places (the import of the “bill of materials” and the integration of the Quarkus Maven plugin). At which position you insert the Quarkus version is technically irrelevant – for the sake of clarity, I insert the version before the PMD version:

<properties>
    . . .
    <quarkus.platform.version>3.4.3</quarkus.platform.version>
    <pmd.version>6.55.0</pmd.version>
    . . .
</properties>Code language: HTML, XML (xml)

Next, we need to integrate the bill of materials we just mentioned. A “bill of materials” is basically a list that defines which versions of which libraries go together. For example, the bill of materials for Quarkus, version 3.4.3, states that Hibernate should be used in version 6.2.13.Final and JUnit in version 5.10.0.

Now, it depends on where you start:

If your starting point is the code we developed together in the previous parts of the tutorial, then you don’t have a <dependencyManagement> block in your pom.xml yet.

On the other hand, if you cloned the project from my GitHub repository, you will find such a block that centrally manages the version numbers of all dependencies.

Why this difference? In the tutorial, I always put the version numbers directly with the dependencies because I found the tutorial more understandable this way than if you had to put an entry for each dependency in the <dependencies> block of the respective module and another entry in the <dependencyManagement> block of the parent POM.

If you already have a <dependencyManagement> block, replace it with the following block. And if you don’t have one yet, just add the following block – e.g., before the existing <dependencies> block:

<dependencyManagement>
    <dependencies>
        <!-- Quarkus "Bill of Materials" -->
        <dependency>
            <groupId>io.quarkus.platform</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>${quarkus.platform.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.tngtech.archunit</groupId>
            <artifactId>archunit-junit5</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>Code language: HTML, XML (xml)

The first entry adds the bill of materials (BoM) mentioned above to the project, and the second defines the ArchUnit version, which is not defined in the Quarkus BoM.

In the <dependencies> block, we can now remove the version numbers for JUnit and Mockito since these versions are already defined in the bill of materials. The versions of Lombok and AssertJ, on the other hand, are not defined in the BoM, so we have to specify them explicitly.

Here you can see the complete <dependencies> block:

<dependencies>
    <!-- Provided scope (shared by all modules) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>

    <!-- Test scope (shared by all modules) -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <!-- JUnit version comes from Quarkus BoM -->
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.24.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <!-- Mockito version comes from Quarkus BoM -->
        <scope>test</scope>
    </dependency>
</dependencies>Code language: HTML, XML (xml)

Adapting the adapter/pom.xml

Next, we change the dependencies in the adapter/pom.xml. We have specified there so far, for example, dependencies to Jakarta RESTful Web Services, Jakarta Persistence, RESTEasy, Hibernate, and the MySQL driver. We now replace all these libraries with the corresponding Quarkus extensions (I’ll show you what the pom.xml will look like afterward).

Quarkus Extensions are small wrappers around the actual libraries that enrich them with some information that allows a Quarkus application to be compiled into a native application using GraalVM.

We are also adding some extensions that we don’t currently need but will need as the migration progresses:

We also need to temporarily leave two of the old libraries in the test scope:

  • RESTEasy Undertow – with this, we start the Undertow web server as long as we haven’t replaced it with Quarkus.
  • Testcontainers/MySQL – with this, we start MySQL in the integration tests. Quarkus also uses test containers later but does not need this specific dependency.

Here you can find the complete <dependencies> block of the adapter/pom.xml:

<dependencies>
    <!-- Internal -->
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>application</artifactId>
        <version>${project.version}</version>
    </dependency>

    <!-- External -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-mysql</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-jackson</artifactId>
    </dependency>

    <!-- Test scope -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5-mockito</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Required temporarily during migration -->
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-undertow</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mysql</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- To use the "attached test JAR" from the "model" module -->
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>model</artifactId>
        <version>${project.version}</version>
        <classifier>tests</classifier>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
</dependencies>
Code language: HTML, XML (xml)

Importing APIs or Implementations?

If you’ve read through the entire tutorial to this point, you may notice that I’ve revised a design decision:

So far, in the adapter module, we had only imported the interfaces (such as Jakarta RESTful Web Services and Jakarta Persistence) in the compile scope and their implementations (such as RESTEasy and Hibernate) in the test scope. Accordingly, we then had to import the implementations in the bootstrap module again in the runtime scope.

The following graphic demonstrates this as an example for Hibernate and Jakarta Persistence:

Module dependencies on Hibernate and Jakarta Persistence – previous model
Module dependencies on Hibernate and Jakarta Persistence – previous model

In the course of the migration to Quarkus, we have now directly imported the implementations in the compile scope in the adapter module so that a) explicit imports of the interfaces are no longer necessary – we now get them as transitive dependencies via the implementations – and b) we do not need to import the implementations additionally in the bootstrap module:

Module dependencies on Hibernate and Jakarta Persistence – new model
Module dependencies on Hibernate and Jakarta Persistence – new model

Which approach is better?

Both have their advantages and disadvantages. In the original approach, we don’t have unnecessary compile-scope dependencies in the adapter. Thus, we would still have the option in the bootstrap module to choose a different implementation (e.g., EclipseLink instead of Hibernate or Jersey instead of RESTEasy).

On the other hand – how likely is it that we want to use different implementations of an API within a project? Quite unlikely! Thus, we can confidently use the second variant here, which requires less code and is therefore clearer and less error-prone.

The first variant might be useful if we want to publish a library based on Jakarta Persistence, for example, that uses a specific JPA implementation for integration testing but leaves it open to the library user which JPA implementation they end up using.

Adapting the bootstrap/pom.xml

Now, let’s move on to the dependencies in bootstrap/pom.xml. Here, we can delete all dependencies in the runtime scope without replacement. We don’t need these anymore because, in the adapter module, we have already imported everything necessary in the compile scope.

In the test scope, we need to add the Quarkus extensions for JUnit and Mockito:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>Code language: HTML, XML (xml)

We leave the dependency to RESTEasy Undertow temporarily so that our application is still executable.

The entire <dependencies> block of bootstrap/pom.xml looks like this:

<dependencies>
    <!-- Internal -->
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>adapter</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- The "application" and "model" modules are transitively included already;
         but we need to include them *explicitly* so that the aggregated JaCoCo report
         will cover them. -->
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>application</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>model</artifactId>
        <version>${project.version}</version>
    </dependency>

    <!-- External -->
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-undertow</artifactId>
        <exclusions>
            <!-- Conflicts with io.smallrye:jandex, a dependency from Hibernate -->
            <exclusion>
                <groupId>org.jboss</groupId>
                <artifactId>jandex</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- Test scope -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5-mockito</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.tngtech.archunit</groupId>
        <artifactId>archunit-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- To use the "attached test JAR" from the "adapter" module -->
    <dependency>
        <groupId>eu.happycoders.shop</groupId>
        <artifactId>adapter</artifactId>
        <version>${project.version}</version>
        <classifier>tests</classifier>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
</dependencies>
Code language: HTML, XML (xml)

Your pom.xml files should now look like this (the links are to the intermediate state after step 1 in the GitHub repository):

You can track all changes in detail via the GitHub commit of step 1.

The project should now compile, and all tests should be green:

mvn clean verifyCode language: plaintext (plaintext)

Note that we still use our own dependency injection mechanism and transaction management. In the next step, we will adapt the code to use the appropriate functionalities of the application framework.

Step 2: Quarkus Dependency Injection in the Adapter Module

One task that an application framework takes care of is dependency injection. So far, we have wired the application manually via constructor injection. In the adapter module, this was still quite clear, as we only ever had to construct those adapters that we wanted to test, and mocked all the use cases.

In the bootstrap module, on the other hand, we had to write a complete launcher that creates and connects all the services, controllers, and repositories and then launches the Undertow web server. For this demo application, that was about a hundred lines of code. For an enterprise application, however, it could well be thousands to tens of thousands of lines.

Dependency Injection with Quarkus

The dependency injection mechanism of Quarkus is based on the CDI standard, i.e., we annotate classes that the framework should instantiate with @ApplicationScoped (there are other annotations, but we do not use them in this tutorial).

In our demo application, we need to do this for the four repository classes from the eu.happycoders.store.adapter.out.persistence package (I’ll show you exactly how we do this in a moment):

  • InMemoryCartRepository
  • InMemoryProductRepository
  • JpaCartRepository
  • JpaProductRepository

Our controller classes from the eu.happycoders.store.adapter.in.rest package are already provided with Jakarta RESTful web services annotations (@Path, @GET, @POST) and are thus automatically instantiated by Quarkus as well.

Configuration In-Memory vs. MySQL Adapters

One of the advantages of Quarkus is that it resolves dependencies between components and generates proxies at build time (rather than at runtime, like Spring and most Jakarta EE frameworks).

Accordingly, Quarkus provides several options to create or not create components configuration-wise, as we need for the in-memory or MySQL adapters:

  • Option 1: at build time via a build profile and @IfBuildProfile annotations to the repository implementations.
  • Option 2: at build time via a build property and @IfBuildProperty annotations to the repository implementations.
  • Option 3: at runtime via @LookupIfProperty-annotations to the repository implementations.

Since the database connection settings are also loaded at runtime, I opt for the third variant.

Accordingly, we now annotate our four repository classes as follows:

@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
@ApplicationScoped
public class InMemoryCartRepository implements CartRepository {
  . . .
}

@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
@ApplicationScoped
public class InMemoryProductRepository implements ProductRepository {
  . . .
}

@LookupIfProperty(name = "persistence", stringValue = "mysql")
@ApplicationScoped
public class JpaCartRepository implements CartRepository {
  . . .
}

@LookupIfProperty(name = "persistence", stringValue = "mysql")
@ApplicationScoped
public class JpaProductRepository implements ProductRepository {
  . . .
}
Code language: Java (java)

What does that mean exactly?

  • If we start our application with the configuration entry persistence=inmemory or without a persistence entry, the in-memory adapters are instantiated.
  • If we start the application with the configuration entry persistence=mysql, the MySQL adapters are instantiated.

We can define configuration entries in Quarkus according to the MicroProfile Config standard. So we can put them in the application.properties, set them via environment variables, or specify them as system properties.

That means that we can configure our application in precisely the same way as we were able to before we switched to Quarkus (e.g., by starting it with the parameter -Dpersistence=mysql).

Removing the Persistence Configuration

Quarkus automatically finds all JPA entities annotated with @Entity. We can, therefore, delete the persistence.xml file in the resources/META-INF directory of the adapter module without replacement – and with it, the entire resources directory, which now no longer contains any files.

That was all we had to change in the actual adapter code. Next, we need to adjust the integration tests.

Quarkus Test Profiles

By specifying "lookupIfMissing = true" in the @LookupIfProperty annotations of the in-memory adapters, we have specified that the in-memory adapters are loaded even if the persistence property is not set. So, in-memory is the standard (as it was before the switch to Quarkus).

Now, to be able to test the MySQL adapters as well, we need to create a Quarkus test profile that sets the persistence property to"mysql".

For this, we create the class TestProfileWithMySQL in the package eu.happycoders.store.adapter of the test directory with the following content:

public class TestProfileWithMySQL implements QuarkusTestProfile {

  @Override
  public Map<String, String> getConfigOverrides() {
    return Map.of("persistence", "mysql");
  }
}
Code language: Java (java)

In the following, we can then annotate integration tests with @TestProfile(TestProfileWithMySQL.class) to start them with the persistence=mysql setting.

Adapting the Repository Tests

Since we’re on test profiles, let’s start customizing the integration tests for the repositories. We will come to the controller tests in the next section.

As a reminder, for the repository tests, we created a class hierarchy in which we defined the actual tests in an abstract base class and created the in-memory and JPA adapters in each of two concrete derived implementations:

Hierarchy of JPA adapter test classes
Hierarchy of JPA adapter test classes

We leave this structure in place, but we can remove some code because we can now leave the creation of the adapter instances to the framework. I’ll show you an example of the changes to the product repository tests below. The Cart repository tests will be adapted analogously, and I will link the corresponding classes in the GitHub repository at the end of the section.

AbstractProductRepositoryTest

Let’s start with the abstract base class, AbstractProductRepositoryTest. Here’s what the class looks like so far:

public abstract class AbstractProductRepositoryTest<T extends ProductRepository> {

  private T productRepository;

  @BeforeEach
  void initRepository() {
    productRepository = createProductRepository();
  }

  protected abstract T createProductRepository();

  . . .
}Code language: Java (java)

We modify the lines up to the three points as follows:

public abstract class AbstractProductRepositoryTest {

  @Inject Instance<ProductRepository> productRepositoryInstance;

  private ProductRepository productRepository;

  @BeforeEach
  void initRepository() {
    productRepository = productRepositoryInstance.get();
  }

  . . .
}Code language: Java (java)

What have we done here in detail?

  • Using the line @Inject Instance<ProductRepository> productRepositoryInstance, we let the framework inject an instance of the ProductRepository interface. The detour via Instance is necessary because the concrete class is not yet known at compile time – at runtime, this could be an InMemoryProductRepository or a JpaProductRepository.
  • In the @BeforeEach method, we load the concrete instance via the Instance.get() method.
  • We no longer need the abstract method createProductRepository() used so far and can delete it accordingly.
  • We also no longer need the type parameter of the class, <T extends ProductRepository>, which we can also remove.

You can find the custom class here in the GitHub repository: AbstractProductRepositoryTest

JpaProductRepositoryTest

Let’s move on to the JPA implementation of the test, JpaProductRepositoryTest. Here’s what the class looks like so far:

class JpaProductRepositoryTest
    extends AbstractProductRepositoryTest<JpaProductRepository> {

  private static MySQLContainer<?> mysql;
  private static EntityManagerFactory entityManagerFactory;

  @BeforeAll
  static void startDatabase() {
    mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.1"));
    mysql.start();

    entityManagerFactory =
        EntityManagerFactoryFactory.createMySqlEntityManagerFactory(
            mysql.getJdbcUrl(), "root", "test");
  }

  @Override
  protected JpaProductRepository createProductRepository() {
    return new JpaProductRepository(entityManagerFactory);
  }

  @AfterAll
  static void stopDatabase() {
    entityManagerFactory.close();
    mysql.stop();
  }
}Code language: Java (java)

We can remove all the boilerplate code and replace it with just two annotations. This is how the class looks after the changeover:

@QuarkusTest
@TestProfile(TestProfileWithMySQL.class)
class JpaProductRepositoryTest extends AbstractProductRepositoryTest {}Code language: Java (java)

Yes, you see correctly: the method does not contain a single method any more! How did we achieve this in detail?

  • We no longer needed the createProductRepository() method in the abstract base class because we are working with the framework’s dependency injection mechanism. So we can also remove it from the implementation.
  • To start the framework, we need to annotate the test class with @QuarkusTest instead.
  • By annotating @TestProfile(TestProfileWithMySQL.class), we specify that Quarkus will create an instance of JpaProductRepository instead of InMemoryProductRepository in the corresponding test profile (see section Configuration In-Memory vs. MySQL Adapters).
  • And since Quarkus recognizes that our integration test needs a database, it automatically boots up a MySQL database via test containers and Docker¹. That also allows us to remove the startDatabase() and startDatabase() methods.

You can find the customized class here in the GitHub repository: JpaProductRepositoryTest

¹ Quarkus recognizes that it should start MySQL and not another database by importing quarkus-jdbc-mysql in the pom.xml. Quarkus starts MySQL 8.0 by default. If you want to start a different version, you can do this via the configuration entry quarkus.datasource.devservices.image-name, e.g., via the entry quarkus.datasource.devservices.image-name=mysql:8.2.0 in the file test/resources/application.properties in the adapter module.

InMemoryRepositoryTest

Let’s move on to the in-memory implementation, InMemoryRepositoryTest, which so far looks like this:

class InMemoryProductRepositoryTest
    extends AbstractProductRepositoryTest<InMemoryProductRepository> {

  @Override
  protected InMemoryProductRepository createProductRepository() {
    return new InMemoryProductRepository();
  }
}Code language: Java (java)

We again remove the createProductRepository() method and add the @QuarkusTest annotation:

@QuarkusTest
class InMemoryProductRepositoryTest extends AbstractProductRepositoryTest {}Code language: Java (java)

We do not need a @TestProfile annotation because the in-memory repository is instantiated by default.

You can find the adapted class here in the GitHub repository: InMemoryProductRepositoryTest

CartRepository Tests

I encourage you to make the appropriate changes for the CartRepository tests once yourself for practice purposes. When you’re done with that, you can compare your results with mine:

Adapting the Controller Tests

Let’s move on to the controller tests. Again, I will only show you the changes using the product controller as an example. The test for the cart controller is changed analogously.

Here’s what the ProductsControllerTest class looks like so far:

class ProductsControllerTest {

  private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99));
  private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99));

  private static final FindProductsUseCase findProductsUseCase = 
      mock(FindProductsUseCase.class);

  private static UndertowJaxrsServer server;

  @BeforeAll
  static void init() {
    server =
        new UndertowJaxrsServer()
            .setPort(TEST_PORT)
            .start()
            .deploy(
                new Application() {
                  @Override
                  public Set<Object> getSingletons() {
                    return Set.of(new FindProductsController(findProductsUseCase));
                  }
                });
  }

  @AfterAll
  static void stop() {
    server.stop();
  }

  @BeforeEach
  void resetMocks() {
    Mockito.reset(findProductsUseCase);
  }

  . . .
}Code language: Java (java)

We modify the lines up to the three points as follows:

@QuarkusTest
class ProductsControllerTest {

  private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99));
  private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99));

  @InjectMock FindProductsUseCase findProductsUseCase;

  . . .
}Code language: Java (java)

We were again able to remove quite a bit of boilerplate code. Specifically, we did the following:

  • We annotated the test class with @QuarkusTest to start the integration test with a running Quarkus application.
  • We no longer mock the FindProductsUseCase explicitly through Mockito.mock() but through the annotation @InjectMock. This annotation creates the mock and instructs Quarkus to inject the mocked use case instance wherever a FindProductsUseCase is needed.
  • We can remove the init()- and stop()-methods because, on the one hand, we don’t need the Undertow web server anymore, and on the other hand, we don’t have to create the FindProductsController manually – due to the Jakarta-RESTful-Web-Services annotations it is recognized by the framework and instantiated automatically. And via the FindProductsController constructor, the mocked FindProductsUseCase is automatically injected.
  • We can also remove the resetMocks() method. Quarkus generates a fresh mock for each test.

In addition, we remove all calls to port(TEST_PORT) from the individual tests. Quarkus automatically ensures that REST Assured calls the application on the correct port.

Again, feel free to try to customize CartsControllerTest yourself. Here you can find the adapted test classes in the GitHub repository:

With that, we should be done with the adapter module changes. However, calling mvn clean verify at this point still results in the following error message (I’ve shortened it to the essentials):

Found 4 deployment problems:
[1] Unsatisfied dependency for type AddToCartUseCase
     - java member: AddToCartController():addToCartUseCase
[2] Unsatisfied dependency for type EmptyCartUseCase
     - java member: EmptyCartController():emptyCartUseCase
[3] Unsatisfied dependency for type FindProductsUseCase
     - java member: FindProductsController():findProductsUseCase
[4] Unsatisfied dependency for type GetCartUseCase
     - java member: GetCartController():getCartUseCase Code language: plaintext (plaintext)

What does this mean, and what must we do to fix this problem? I’ll explain that in the next section.

Defining Service Components

The classes AddToCartController, EmptyCartController, FindProductsController, and GetCartController each have a constructor into which an instance of AddToCartUseCase, EmptyCartUseCase, FindProductsUseCase, and GetCartUseCase must be injected.

Although we only need the mocked components in our integration tests, Quarkus cannot detect that – so to boot the application, it needs all the components.

We, therefore, need to define at this point how Quarkus can create instances of the use case interfaces.

One possibility would be to annotate the classes AddToCartUseService, EmptyCartUseService, FindProductsService, and GetCartUseService each with @ApplicationScoped in the application module. But for this, we would have to “pollute” the application module with a dependency on Quarkus. But we don’t want to do that – the application module shall remain free of any technical details!

Therefore, we create a class QuarkusAppConfig in the adapter module in the package eu.happycoders.store (the name does not matter), which has the following content:

class QuarkusAppConfig {

  @Inject Instance<CartRepository> cartRepository;
  @Inject Instance<ProductRepository> productRepository;

  @Produces
  @ApplicationScoped
  GetCartUseCase getCartUseCase() {
    return new GetCartService(cartRepository.get());
  }

  @Produces
  @ApplicationScoped
  EmptyCartUseCase emptyCartUseCase() {
    return new EmptyCartService(cartRepository.get());
  }

  @Produces
  @ApplicationScoped
  FindProductsUseCase findProductsUseCase() {
    return new FindProductsService(productRepository.get());
  }

  @Produces
  @ApplicationScoped
  AddToCartUseCase addToCartUseCase() {
    return new AddToCartService(cartRepository.get(), productRepository.get());
  }
}
Code language: Java (java)

The methods annotated with @Produces and @ApplicationScope generate our four services. We let Quarkus inject the repositories we need to be injected into the services via the two fields annotated with @Inject.

Quarkus detects all dependencies and creates the components accordingly in the following order:

  1. First, the in-memory and JPA repositories annotated with @ApplicationScope.
  2. Then, the services defined via the Producer methods.
  3. And lastly, the controllers annotated with @Path, @POST, and @GET.

Now the project should compile and test without errors again:

mvn clean verifyCode language: plaintext (plaintext)

Finally, we can clean up the adapter module’s pom.xml and remove the dependencies to resteasy-undertow and org.testcontainers:mysql.

You can track all changes in detail via the GitHub commit of step 2.

It is debatable whether the definition of the service classes belongs in the adapter module or the bootstrap module. Since the adapter module doesn’t necessarily need the service implementations (but only mocks for integration testing), we could also have the producer methods return dummy implementations of the use cases and then define the final producer methods in the bootstrap module that create the services.

However, this would have some messy consequences: Since the bootstrap module imports the adapter module and the bootstrap tests import the adapter tests, the adapter module’s producer methods would also be visible in the bootstrap module. In the bootstrap module, there would then be two components of the same interface, and Quarkus would not launch.

To resolve this, we would have two options: We could forgo importing the adapter tests into the bootstrap tests – but then we would have to duplicate many helper methods from the adapter tests into the bootstrap tests. Alternatively, we could create two separate test profiles for the adapter tests and add an @IfBuildProfile annotation to the QuarkusAppConfig class in the adapter module.

Both options require a lot of overhead, so I ultimately chose the simpler solution and defined the services in the adapter module.

Step 3: Quarkus in the Boostrap Module

At this stage, we have an application whose adapter classes are executable within integration tests with Quarkus but which is still launched with an Undertow web server in the bootstrap module. That is possible because we worked exclusively with Jakarta standards.

Nevertheless, we now want to switch the bootstrap module to Quarkus as well. Fortunately, this is much faster than changing the adapter module.

Let’s start with the tests...

Adapting End-to-End Tests

I’ll show you the necessary adjustments again using the example of the product test, class FindProductsTest. This is how the class definition currently looks:

class FindProductsTest extends EndToEndTest {
  . . .
}Code language: Java (java)

We replace the header with:

@QuarkusTest
@TestProfile(TestProfileWithMySQL.class)
class FindProductsTest {
  . . .
}Code language: Java (java)

What have we changed in detail?

  • We added the @QuarkusTest annotation to run the test with a started Quarkus application.
  • Accordingly, the test class no longer needs to inherit from the parent class EndToEndTest, which was previously responsible for wiring the components and starting the application.
  • Using the annotation @TestProfile(TestProfileWithMySQL.class), we start the application in MySQL mode, so the end-to-end testing includes persisting the data to the database.

In addition, we again remove all calls to port(TEST_PORT) from the individual tests.

Again, feel free to try to customize CartTest yourself. Here you can find the adapted test classes in the GitHub repository:

Now, we don’t need the following classes anymore; we can delete them all:

  • the former parent class of the tests, EndToEndTest,
  • the starter classes Launcher and RestEasyUndertowShopApplication, which start the application with the Undertow web server (so the bootstrap module no longer contains any Java code),
  • the EntityManagerFactoryFactory from the adapter module,
  • the TEST_PORT constant from the HttpTestCommons class of the adapter module.

In addition, we can remove the dependency to resteasy-undertow from the bootstrap module’s pom.xml.

A call to mvn clean verify shows that the adapted tests are running successfully.

But how can we start our Quarkus application now – without the just deleted starter classes?

Starting the Quarkus Application

We do this via the Quarkus Maven plugin. This creates a startable application and also offers us the possibility to start Quarkus in the so-called “”dev mode”.

First, we add the plugin to the <plugins> block of the parent pom.xml as follows:

<plugin>
    <groupId>io.quarkus.platform</groupId>
    <artifactId>quarkus-maven-plugin</artifactId>
    <version>${quarkus.platform.version}</version>
</plugin>
Code language: HTML, XML (xml)

Second, we add the following <build> block to the bootstrap module’s pom.xml (e.g., after the <dependencies> block):

<build>
    <plugins>
        <plugin>
            <groupId>io.quarkus.platform</groupId>
            <artifactId>quarkus-maven-plugin</artifactId>
            <extensions>true</extensions>
            <executions>
                <execution>
                    <goals>
                        <goal>build</goal>
                        <goal>generate-code</goal>
                        <goal>generate-code-tests</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

With the <executions> block, we tell Maven to run the plugin in the three phases listed.

Now, we can start the application in dev mode using the following command:

mvn quarkus:devCode language: plaintext (plaintext)

After a few seconds, you should see the following:

hexagonal architecture quarkus demo

You can now try out the application’s endpoints, as described in the “Starting the Application” section of the second part of the series.

For those readers who are not familiar with Quarkus, I would like to point out a remarkable feature of the Dev Mode: Changes you make to the code now will immediately affect the running application – without having to manually recompile and restart it – give it a try!

You can track all changes from step 3 via the corresponding GitHub commit.

Step 4: @Transactional and Panache

We now have a running application, but we still have one construction site:

In the JPA repositories in the adapter module, we left some boilerplate code behind – namely, the one for transaction management and loading and storing entities. Again, Quarkus can take some of the overhead off our hands.

Panache Repositories for CRUD Operations

We first create a Panache repository for each of our two entity types. A Panache repository implements basic CRUD operations (analogous to Spring Data JPA’s CrudRepository).

We create the following two classes in the eu.happycoders.store.adapter.out.persistence.jpa package of the adapter module:

@ApplicationScoped
public class JpaCartPanacheRepository
    implements PanacheRepositoryBase<CartJpaEntity, Integer> {}

@ApplicationScoped
public class JpaProductPanacheRepository
    implements PanacheRepositoryBase<ProductJpaEntity, String> {}Code language: Java (java)

Both classes implement the interface PanacheRepositoryBase and specify as type parameters the types of the entities to be stored and their primary keys.

Adapting JpaCartRepository

In the class JpaCartRepository, we now replace the following lines:

private final EntityManagerFactory entityManagerFactory;

public JpaCartRepository(EntityManagerFactory entityManagerFactory) {
  this.entityManagerFactory = entityManagerFactory;
}
Code language: Java (java)

by:

private final JpaCartPanacheRepository panacheRepository;

public JpaCartRepository(JpaCartPanacheRepository panacheRepository) {
  this.panacheRepository = panacheRepository;
}
Code language: Java (java)

and the following three methods:

@Override
public void save(Cart cart) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    entityManager.getTransaction().begin();
    entityManager.merge(CartMapper.toJpaEntity(cart));
    entityManager.getTransaction().commit();
  }
}

@Override
public Optional<Cart> findByCustomerId(CustomerId customerId) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    CartJpaEntity cartJpaEntity = 
        entityManager.find(CartJpaEntity.class, customerId.value());
    return CartMapper.toModelEntityOptional(cartJpaEntity);
  }
}

@Override
public void deleteByCustomerId(CustomerId customerId) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    entityManager.getTransaction().begin();

    CartJpaEntity cartJpaEntity = 
        entityManager.find(CartJpaEntity.class, customerId.value());

    if (cartJpaEntity != null) {
      entityManager.remove(cartJpaEntity);
    }

    entityManager.getTransaction().commit();
  }
}Code language: Java (java)

by:

@Override
@Transactional
public void save(Cart cart) {
  panacheRepository.getEntityManager().merge(CartMapper.toJpaEntity(cart));
}

@Override
@Transactional
public Optional<Cart> findByCustomerId(CustomerId customerId) {
  CartJpaEntity cartJpaEntity = panacheRepository.findById(customerId.value());
  return CartMapper.toModelEntityOptional(cartJpaEntity);
}

@Override
@Transactional
public void deleteByCustomerId(CustomerId customerId) {
  panacheRepository.deleteById(customerId.value());
}Code language: Java (java)

We have replaced the manual starting and committing of transactions with a @Transactional annotation and the cumbersome use of the EntityManager with a much more convenient use of the Panache repository.

Adapting JpaProductRepository

In the class JpaProductRepository, we proceed analogously – we replace the following lines:

private final EntityManagerFactory entityManagerFactory;

public JpaProductRepository(EntityManagerFactory entityManagerFactory) {
  this.entityManagerFactory = entityManagerFactory;
  createDemoProducts();
}

private void createDemoProducts() {
  DemoProducts.DEMO_PRODUCTS.forEach(this::save);
}
Code language: Java (java)

through

private final JpaProductPanacheRepository panacheRepository;

public JpaProductRepository(JpaProductPanacheRepository panacheRepository) {
  this.panacheRepository = panacheRepository;
}

@PostConstruct
void createDemoProducts() {
  DemoProducts.DEMO_PRODUCTS.forEach(this::save);
}Code language: Java (java)

Note that we are no longer allowed to call createDemoProducts() in the constructor because when the constructor is called, the application is not yet in a state where it can access the database via the Panache repositories.

Instead, we annotate the createDemoProducts() method with @PostConstruct, which tells Quarkus to call the method once all the application components have been created and wired (just as you would in Spring).

And lastly, we substitute the following three methods:

@Override
public void save(Product product) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    entityManager.getTransaction().begin();
    entityManager.merge(ProductMapper.toJpaEntity(product));
    entityManager.getTransaction().commit();
  }
}

@Override
public Optional<Product> findById(ProductId productId) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    ProductJpaEntity jpaEntity =
        entityManager.find(ProductJpaEntity.class, productId.value());
    return ProductMapper.toModelEntityOptional(jpaEntity);
  }
}

@Override
public List<Product> findByNameOrDescription(String queryString) {
  try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
    TypedQuery<ProductJpaEntity> query =
        entityManager
            .createQuery(
                "from ProductJpaEntity "
                        + "where name like :query or description like :query",
                ProductJpaEntity.class)
            .setParameter("query", "%" + queryString + "%");

    List<ProductJpaEntity> entities = query.getResultList();

    return ProductMapper.toModelEntities(entities);
  }
}Code language: Java (java)

through

@Override
@Transactional
public void save(Product product) {
  panacheRepository.getEntityManager().merge(ProductMapper.toJpaEntity(product));
}

@Override
@Transactional
public Optional<Product> findById(ProductId productId) {
  ProductJpaEntity jpaEntity = panacheRepository.findById(productId.value());
  return ProductMapper.toModelEntityOptional(jpaEntity);
}

@Override
@Transactional
public List<Product> findByNameOrDescription(String queryString) {
  List<ProductJpaEntity> entities =
      panacheRepository
          .find("name like ?1 or description like ?1", "%" + queryString + "%")
          .list();

  return ProductMapper.toModelEntities(entities);
}Code language: Java (java)

Again, by using the @Transactional annotation and the Panache repository, we were able to remove a lot of boilerplate code.

With a call to mvn clean verify, we can confirm that our repositories continue to do what they are supposed to do.

Here you can find all classes added or changed in this step in the GitHub repository:

You can also find all the changes from step 4 in this GitHub commit.

Building and Launching the Application in Production Mode

So far, we have only started the application via mvn quarkus:dev in Quarkus dev mode. But how can we build and launch the application in production mode?

Configuring Production Mode

To start our application in production mode, we need to configure the database connection. As this depends on the environment, we usually do this using environment variables. For our small demo application, however, it is easier to store the parameters in the application.properties file.

Create an application.properties file in the bootstrap/src/main/resources directory with the following content:

%prod.quarkus.datasource.jdbc.url=dummy
%prod.persistence=inmemory

%mysql.quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/shop
%mysql.quarkus.datasource.username=root
%mysql.quarkus.datasource.password=test
%mysql.quarkus.hibernate-orm.database.generation=update
%mysql.persistence=mysqlCode language: plaintext (plaintext)

With the prefix %prod or %mysql, we define a property for a specific profile.

Configuring Production Mode – In-Memory Mode

The “prod” profile is the standard profile for production mode – here we want to use in-memory mode.

Unfortunately, we also have to specify a JDBC URL in in-memory mode. Without this, Quarkus would abort with the following error message:

Model classes are defined for the default persistence unit but configured datasource not found: the default EntityManagerFactory will not be created.

Quarkus would be bothered by the fact that entity classes exist in the code but that no database connection is defined. Unfortunately, this is an unattractive aspect of our demo application. In an actual application, it is unlikely that we will define JPA entities but not connect the application to a database.

Configuring Production Mode – MySQL Mode

In the “mysql” profile, we want to use MySQL mode and specify the MySQL connection data for the test database (which we will start via Docker below).

With the parameter quarkus.hibernate-orm.database.generation=update, we define that Hibernate should automatically create all database tables and update them if necessary. This parameter was automatically set in the tests and in dev mode.

In production, we should never use this setting and instead use a tool like Flyway or Liquibase. But for our demo application, the additional work involved is too great.

Building the Quarkus Application

We build the application using the Quarkus Maven plugin via the following command:

mvn clean packageCode language: plaintext (plaintext)

After that, you will find the executable app in the bootstrap/target/quarkus-app directory.

Starting the Quarkus Application in In-Memory Mode

You can now start the application in in-memory mode with the following command:

cd bootstrap/target/quarkus-app
java -jar quarkus-run.jarCode language: plaintext (plaintext)

You will see a warning that the dummy JDBC URL defined in application.properties is invalid. You can ignore this warning as the application does not connect to a database in this mode.

Starting the Quarkus Application in MySQL Mode

To start the application in MySQL mode, you must first start a database. You can do this with the following Docker command (note: if you are using Windows, you need to write everything on one line and remove the backslashes):

docker run --name hexagon-mysql -d -p3306:3306 \
    -e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=test mysql:8.2Code language: plaintext (plaintext)

After that, you start the application as follows:

java -jar -Dquarkus.profile=mysql quarkus-run.jarCode language: plaintext (plaintext)

Configuring the Database Connection via System Properties or Environment Variables

If your database parameters are different, you can change them in the application.properties or specify alternative values at runtime.

One way to do this is via system properties, e.g. as follows:

java -jar \
    -Dquarkus.profile=mysql \
    -Dquarkus.datasource.jdbc.url=jdbc:mysql://<hostname and port>/<database name> \
    -Dquarkus.datasource.username=<your username> \
    -Dquarkus.datasource.password=<your password> \
    quarkus-run.jarCode language: plaintext (plaintext)

Alternatively, you can define the settings via environment variables (on Windows, you have to use set instead of export):

export QUARKUS_DATASOURCE_JDBC_URL=jdbc:mysql://localhost:3306/shop
export QUARKUS_DATASOURCE_USERNAME=root
export QUARKUS_DATASOURCE_PASSWORD=test
export QUARKUS_HIBERNATE_ORM_DATABASE_GENERATION=update
export PERSISTENCE=mysql
java -jar quarkus-run.jarCode language: plaintext (plaintext)

Launching the Quarkus Application With Docker

You can also very easily package the application into a Docker image. To do this, create a file named Dockerfile.jvm in the bootstrap/src/main/docker directory with the following content:

FROM eclipse-temurin:20

ENV LANGUAGE='en_US:en'

COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 185
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar $JAVA_APP_JAR"]
Code language: Dockerfile (dockerfile)

Then execute the following command in the project directory:

docker build -f bootstrap/src/main/docker/Dockerfile.jvm \
    -t happycoders/shop-demo bootstrapCode language: plaintext (plaintext)

After that, you can start the Docker image, for example, as follows:

docker run -p 8080:8080 happycoders/shop-demo
Code language: plaintext (plaintext)

Now, for example, we could create a docker-compose.yml file that will boot up a MySQL database and our demo shop in MySQL mode. But since this is a hexagonal architecture tutorial and not a Docker tutorial, I’ll leave further experimentation on Docker to you.

Summary and Outlook

In this part of the hexagonal architecture tutorial series, we migrated our demo shop application developed in the previous parts to the Quarkus framework. This allowed us to unify the dependencies and remove a lot of boilerplate code.

An application framework also prepares our application for possible use in production – we could now make our application "production-ready" with relatively manageable effort:

During the migration to Quarkus, the great advantage of the hexagonal architecture became apparent once again: We did not need to change one line of code in the core of the application – and thus, even the application framework in the hexagonal architecture is just a replaceable technical detail.

And that’s what we will take advantage of in the next part of this tutorial series: We will replace Quarkus with the Spring Framework.

If you liked the article, I would appreciate a short review on ProvenExpert.

Do you want to stay up to date and be informed when new articles are published on HappyCoders.eu? Then click here to sign up for the free HappyCoders newsletter.