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:
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:
- First, we will insert the Quarkus dependencies and remove the direct dependencies to those libraries brought by Quarkus.
- Then we will adapt the adapter module to Quarkus by using the dependency injection mechanism of Quarkus instead of injecting the dependencies manually.
- In the third step, we will do the same for the bootstrap module.
- 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.
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).
We are also adding some extensions that we don’t currently need but will need as the migration progresses:
- Quarkus ArC – the Jakarta Contexts and Dependency Injection (CDI) based dependency injection mechanism of Quarkus.
- Panache – a library that – similar to Spring Data JPA – lets us implement JPA repositories quickly and easily.
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:
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:
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):
- pom.xml in the root directory
- adapter/pom.xml
- bootstrap/pom.xml
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 verify
Code 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:
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 theProductRepository
interface. The detour viaInstance
is necessary because the concrete class is not yet known at compile time – at runtime, this could be anInMemoryProductRepository
or aJpaProductRepository
. - In the
@BeforeEach
method, we load the concrete instance via theInstance.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 ofJpaProductRepository
instead ofInMemoryProductRepository
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()
andstartDatabase()
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 throughMockito.mock()
but through the annotation@InjectMock
. This annotation creates the mock and instructs Quarkus to inject the mocked use case instance wherever aFindProductsUseCase
is needed. - We can remove the
init()
- andstop()
-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 theFindProductsController
manually – due to the Jakarta-RESTful-Web-Services annotations it is recognized by the framework and instantiated automatically. And via theFindProductsController
constructor, the mockedFindProductsUseCase
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:
- First, the in-memory and JPA repositories annotated with
@ApplicationScope
. - Then, the services defined via the Producer methods.
- And lastly, the controllers annotated with
@Path
,@POST
, and@GET
.
Now the project should compile and test without errors again:
mvn clean verify
Code 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.
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
andRestEasyUndertowShopApplication
, 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 theHttpTestCommons
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:dev
Code language: plaintext (plaintext)
After a few seconds, you should see the following:
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=mysql
Code 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 package
Code 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.jar
Code 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.2
Code language: plaintext (plaintext)
After that, you start the application as follows:
java -jar -Dquarkus.profile=mysql quarkus-run.jar
Code 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.jar
Code 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.jar
Code 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 bootstrap
Code 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:
- Through SmallRye Health, we could make the status of the application retrievable.
- Through SmallRye Metrics or Micrometer Metrics, we could provide metrics.
- Through SmallRye Fault Tolerance, we could make the dependency on external services like the database more resilient.
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.