ports and adapters feature imageports and adapters feature image
HappyCoders Glasses

Ports and Adapters Java Tutorial: Adding a Database Adapter

Sven Woltmann
Sven Woltmann
Last update: November 29, 2024

In the previous, second part of this series about the hexagonal architecture (whose official name is “Ports and Adapters”), I demonstrated how to implement a Java application using this architecture – initially without an application framework and a database. Instead, we implemented a simple dependency mechanism and used an in-memory repository as a persistence adapter.

In this third part, we will implement an adapter that stores the data – shopping carts and products of an online store – in a database. And to emphasize the flexibility of our architecture, we will not simply replace the adapter but add it and make the desired adapter (in-memory or database) selectable via a system property.

The architecture of our application currently looks like this (the port and adapter we are talking about today are highlighted):

Ports-and-Adapters Architecture of the Sample Application
Ports-and-adapters architecture of the sample application

The initial implementation without a database now allows us – in keeping with the ports-and-adapters architecture – to decide which database and schema to use based on the experience we have gained in developing the application core.

Shopping carts and products can be stored very well in a relational database. Therefore, we will use MySQL for persistence and JPA for O/R mapping. The following graphic shows our target architecture:

Ports-and-adapters architecture with two persistence adapters
Ports-and-adapters architecture with two persistence adapters

And we will implement all of this – again, in the spirit of the ports-and-adapters architecture – without changing a single line of code in the application core.

Technologies Used

We will use the following additional technologies to enhance our application:

  • Hibernate as OR mapper (implementation of the Jakarta Persistence API – JPA),
  • Testcontainers, a framework that allows us to launch a MySQL database as a Docker container from tests.

Using test containers, we will implement integration tests for the Hibernate adapters.

You can find the source code for the article in this GitHub repository.

Secondary Ports and Adapters

As a refresher, I’ll show you the current state of the secondary ports and adapters again in the following diagram. (We won’t change anything about the primary ports and adapters in this part of the series – so I’ve hidden them in the upper part of the class diagram.)

Secondary ports and adapters of the sample application
Secondary ports and adapters of the sample application

Today, we will add the eu.happycoders.shop.adapter.out.persistence.jpa package. The following class diagram shows this package with two new adapters – JpaCartRepository and JpaProductRepository – in the lower right pane:

Secondary ports and adapters with two persistence adapters
Secondary ports and adapters with two persistence adapters

Before we start with the implementation, I would like to talk about the topic of mapping, which I covered here in the first part of the series.

We will use two-way mapping here: We will create a corresponding JPA entity for each model entity and then map to the other entity class both when persisting an entity and when loading it from the database.

Implementing the Adapter Classes

The following screenshot shows the adapter module in the IDE extended by all new classes (I have collapsed the package containing the primary adapters, in.rest):

Ports and Adapters with Java – Adapter Classes

Let’s start with the implementation of the JPA entities...

Implementing the JPA Entity Classes

In order to implement the JPA classes, we first need a dependency on JPA. We add that with the following entry in the pom.xml of the adapter module:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
</dependency>
Code language: HTML, XML (xml)

Note that we don’t need a dependency on Hibernate at this point. Hibernate is an implementation of JPA, so we don’t need it until runtime. We will add Hibernate (and some additional required dependencies, like the MySQL JDBC driver) in the test scope as we go along – and again later in the bootstrap module.

ProductJpaEntity

Let’s start with the JPA entity for the product. As a reminder, here is the corresponding entity class, Product (the link is to the GitHub repository), from the model module:

@Data
@Accessors(fluent = true)
@AllArgsConstructor
public class Product {

  private final ProductId id;
  private String name;
  private String description;
  private Money price;
  private int itemsInStock;
}Code language: Java (java)

ProductId is a record that contains only a string. Using such a value object has two advantages: On the one hand, it brings type safety; on the other hand, we can ensure the validity of a product ID in the value object’s constructor.

We now create a corresponding JPA entity class, ProductJpaEntity, in the eu.happycoders.store.adapter.out.persistence.jpa package:

@Entity
@Table(name = "Product")
@Getter
@Setter
public class ProductJpaEntity {

  @Id private String id;

  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  private String description;

  @Column(nullable = false)
  private String priceCurrency;

  @Column(nullable = false)
  private BigDecimal priceAmount;

  private int itemsInStock;
}Code language: Java (java)

The JPA entity differs from the model entity in a few ways:

  • The JPA technical annotations @Entity and @Table make the class a JPA entity that maps to the database table “Product.”
  • The ID marked with @Id is a string, not a ProductId record.
  • All non-primitive mandatory fields are annotated with @Column(nullable = false) to prevent null values from mistakenly creeping into the database.
  • The price, which was of type Money in the model class, is – in the absence of a corresponding SQL type – split into two fields: priceCurrency and priceAmount.

CartLineItemJpaEntity

As a reminder, here is the CartLineItem (an item in the shopping cart) from the model module:

@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
@AllArgsConstructor
public class CartLineItem {

  private final Product product;
  private int quantity;

  // ... 
}Code language: Java (java)

The model class also contains methods to increase a product’s number and calculate the total price of a shopping cart item.

The corresponding JPA class, CartLineItemJpaEntity, is also in the eu.happycoders.store.adapter.out.persistence.jpa package. The JPA entity does not require any corresponding methods since its only purpose is persistence.

@Entity
@Table(name = "CartLineItem")
@Getter
@Setter
public class CartLineItemJpaEntity {

  @Id @GeneratedValue private Integer id;

  @ManyToOne private CartJpaEntity cart;

  @ManyToOne private ProductJpaEntity product;

  private int quantity;
}Code language: Java (java)

We see some more JPA annotations here:

  • @GeneratedValue at the primary key id ensures that this field is automatically filled by MySQL using ascending numbers. Since this primary key is only relevant to the database, no corresponding field exists in the model class.
  • @ManyToOne to cart defines an N:1 relationship to the shopping cart and finally leads to creating a column named cart_id in the database table CartLineItem and a foreign key relationship to the Cart table.
  • @ManyToOne at product accordingly defines an N:1 relationship to the product.

CartJpaEntity

And last, again, as a reminder, the Cart class from the model module:

@Accessors(fluent = true)
@RequiredArgsConstructor
public class Cart {

  @Getter private final CustomerId id; // cart ID = customer ID
  private final Map<ProductId, CartLineItem> lineItems = new LinkedHashMap<>();

  // ...
}Code language: Java (java)

The model class also contains methods to add products, retrieve the shopping cart entries, and calculate the total value of the shopping cart.

Here is the corresponding JPA class, CartJpaEntity:

@Entity
@Table(name = "Cart")
@Getter
@Setter
public class CartJpaEntity {

  @Id private int customerId;

  @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<CartLineItemJpaEntity> lineItems;
}
Code language: Java (java)

Here, I have used another JPA annotation:

  • @OneToMany to lineItems causes access to this field to read all entries of the CartLineItem table referencing this shopping cart and store them in this list. And vice versa, when saving a shopping cart, all entries in this list are written to the CartLineItem table.

With this, we have implemented all the JPA classes we need. Now, let’s move on to the mappers.

Implementing the Model-JPA Mapper Classes

Let's start again with the product...

ProductMapper

The ProductMapper converts a Product into a ProductJpaEntity and vice versa. The mapper is in the same package as the JPA entities, eu.happycoders.store.adapter.out.persistence.jpa. Since the adapters will also be located here, the mapper and its methods do not need to be public.

final class ProductMapper {

  private ProductMapper() {}

  static ProductJpaEntity toJpaEntity(Product product) {
    ProductJpaEntity jpaEntity = new ProductJpaEntity();

    jpaEntity.setId(product.id().value());
    jpaEntity.setName(product.name());
    jpaEntity.setDescription(product.description());
    jpaEntity.setPriceCurrency(product.price().currency().getCurrencyCode());
    jpaEntity.setPriceAmount(product.price().amount());
    jpaEntity.setItemsInStock(product.itemsInStock());

    return jpaEntity;
  }

  static Optional<Product> toModelEntityOptional(ProductJpaEntity jpaEntity) {
    return Optional.ofNullable(jpaEntity).map(ProductMapper::toModelEntity);
  }

  static Product toModelEntity(ProductJpaEntity jpaEntity) {
    return new Product(
        new ProductId(jpaEntity.getId()),
        jpaEntity.getName(),
        jpaEntity.getDescription(),
        new Money(
            Currency.getInstance(jpaEntity.getPriceCurrency()), 
            jpaEntity.getPriceAmount()),
        jpaEntity.getItemsInStock());
  }

  static List<Product> toModelEntities(List<ProductJpaEntity> jpaEntities) {
    return jpaEntities.stream().map(ProductMapper::toModelEntity).toList();
  }
}Code language: Java (java)

The final keyword on the class and the private constructor enforce that this class is not instantiated. Here, I follow a recommendation of static code analysis tools. Apart from that, the mapper class should need no further explanation.

CartMapper

As a reminder, as a secondary port for shopping carts, we only have a CartRepository that persists shopping carts along with their entries. We do not have a separate CartLineItemRepository.

Accordingly, we will convert both Cart and CartLineItem to and from CartJpaEntity and CartLineItemJpaEntity in a shared mapper class, CartMapper. First, here is the code for converting model classes to JPA classes:

final class CartMapper {

  private CartMapper() {}

  static CartJpaEntity toJpaEntity(Cart cart) {
    CartJpaEntity cartJpaEntity = new CartJpaEntity();
    cartJpaEntity.setCustomerId(cart.id().value());

    cartJpaEntity.setLineItems(
        cart.lineItems().stream()
            .map(lineItem -> toJpaEntity(cartJpaEntity, lineItem))
            .toList());

    return cartJpaEntity;
  }

  static CartLineItemJpaEntity toJpaEntity(
      CartJpaEntity cartJpaEntity, CartLineItem lineItem) {
    ProductJpaEntity productJpaEntity = new ProductJpaEntity();
    productJpaEntity.setId(lineItem.product().id().value());

    CartLineItemJpaEntity entity = new CartLineItemJpaEntity();
    entity.setCart(cartJpaEntity);
    entity.setProduct(productJpaEntity);
    entity.setQuantity(lineItem.quantity());

    return entity;
  }

  // method toModelEntityOptional(…), see below

}
Code language: Java (java)

We do not need to set the primary key CartLineItemJpaEntity.id; MySQL does this automatically, as mentioned above.

When converting in the other direction, i.e., from JPA classes to model classes, we have to consider the following particularity: The method Cart.addProduct(...) or the method CartLineItem.increaseQuantityBy(...) called by it throws a NotEnoughItemsInStockException, should a product not be available in sufficient quantity. Of course, this should not happen when we load a shopping cart from the database.

Accordingly, we need yet another method in Cart that bypasses this check:

public void putProductIgnoringNotEnoughItemsInStock(Product product, int quantity) {
  lineItems.put(product.id(), new CartLineItem(product, quantity));
}
Code language: Java (java)

We can then use this method in CartMapper.toModelEntityOptional(…) to convert a CartJpaEntity into a Cart:

static Optional<Cart> toModelEntityOptional(CartJpaEntity cartJpaEntity) {
  if (cartJpaEntity == null) {
    return Optional.empty();
  }

  CustomerId customerId = new CustomerId(cartJpaEntity.getCustomerId());
  Cart cart = new Cart(customerId);

  for (CartLineItemJpaEntity lineItemJpaEntity : cartJpaEntity.getLineItems()) {
    cart.putProductIgnoringNotEnoughItemsInStock(
        ProductMapper.toModelEntity(lineItemJpaEntity.getProduct()),
        lineItemJpaEntity.getQuantity());
  }

  return Optional.of(cart);
}Code language: Java (java)

With this, we have implemented all the mappers we need. Now, let’s move on to the actual adapters.

Implementing the JPA Adapters

Let's start again with the product...

JpaProductRepository

As a reminder, the port responsible for products, ProductRepository, defines three methods:

public interface ProductRepository {

  void save(Product product);

  Optional<Product> findById(ProductId productId);

  List<Product> findByNameOrDescription(String query);
}
Code language: Java (java)

In the last part of the series, we implemented this interface in the in-memory adapter InMemoryProductRepository. In this part, we add the JPA adapter, JpaProductRepository, in the eu.happycoders.store.adapter.out.persistence.jpa package (that is, in the same package as the JPA entities and mappers).

We start with the basic framework of the class. First, we need an EntityManagerFactory for JPA, which we inject via the constructor. In the constructor, we also call the method createDemoProducts(), which – just like in InMemoryProductRepository – creates some test products:

public class JpaProductRepository implements ProductRepository {

  private final EntityManagerFactory entityManagerFactory;

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

  private void createDemoProducts() {
    DemoProducts.DEMO_PRODUCTS.forEach(this::save);
  }

  // ... interface methods ...
}
Code language: Java (java)

Now, we can create the methods defined in the port interface. The save(…) method first creates an EntityManager within a try-with-resources block, starts a transaction, converts the Product to a ProductJpaEntity, stores it in the database via EntityManager.merge(…), and finally commits the transaction. The end of the try-with-resources block closes the EntityManager.

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

The two other methods also first create an EntityManager. The method findById(…) loads a ProductJpaEntity via EntityManager.find(…) and converts it to a Optional<Product> via ProductMapper:

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

findByNameOrDescription(…) creates a TypedQuery via EntityManager.createQuery(…), then loads the ProductJpaEntity list corresponding to the search from the database via TypedQuery.getResultList() and finally uses ProductMapper to convert it to a Product list:

@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)

If you’ve worked with JPA before, this probably all seems relatively cumbersome. That is because most frameworks provide us with an EntityManager and with transaction management, on the one hand, and simplify the creation of custom queries, e.g., via annotations, on the other.

In the two subsequent parts of this tutorial, you will see how the repositories will contain significantly less boilerplate code by using Spring or Quarkus.

JpaCartRepository

The port responsible for shopping carts, CartRepository, also defines three methods:

public interface CartRepository {

  void save(Cart cart);

  Optional<Cart> findByCustomerId(CustomerId customerId);

  void deleteByCustomerId(CustomerId customerId);
}Code language: Java (java)

We now extend the InMemoryCartRepository adapter implemented in the previous part of the series with a JPA adapter, JpaCartRepository, again in the eu.happycoders.store.adapter.out.persistence.jpa package.

We again inject an EntityManagerFactory via the constructor and implement save(…) and findByCustomerId(…) analogous to the corresponding methods in ProductJpaRepository:

public class JpaCartRepository implements CartRepository {

  private final EntityManagerFactory entityManagerFactory;

  public JpaCartRepository(EntityManagerFactory entityManagerFactory) {
    this.entityManagerFactory = entityManagerFactory;
  }

  @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);
    }
  }

  // method deleteByCustomerId(…), see below
}
Code language: Java (java)

The deleteByCustomerId(…) method first searches for the corresponding shopping cart via EntityManager.find(…) and then deletes it from the database via EntityManager.remove(…). The cascade = CascadeType.ALL parameter at the @OneToMany annotation of the CartJpaEntity ensures that all associated CartLineItemJpaEntity records are also deleted.

@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)

This also completes the implementation of the adapters.

EntityManagerFactoryFactory and persistence.xml

We need one more class to create the EntityManagerFactory injected into the constructors of the adapters. We do this via the EntityManagerFactoryFactory, which is in the same package as the adapters:

public final class EntityManagerFactoryFactory {

  private EntityManagerFactoryFactory() {}

  public static EntityManagerFactory createMySqlEntityManagerFactory(
      String jdbcUrl, String user, String password) {
    return Persistence.createEntityManagerFactory(
        "eu.happycoders.shop.adapter.out.persistence.jpa",
        Map.of(
            "hibernate.dialect",                 "org.hibernate.dialect.MySQLDialect",
            "hibernate.hbm2ddl.auto",            "update",
            "jakarta.persistence.jdbc.driver",   "com.mysql.jdbc.Driver",
            "jakarta.persistence.jdbc.url",      jdbcUrl,
            "jakarta.persistence.jdbc.user",     user,
            "jakarta.persistence.jdbc.password", password));
  }
}Code language: Java (java)

The map entry “hibernate.hbm2ddl.auto = update” automatically creates all required database tables. This setting is only suitable for small demo applications like this and for integration tests. For productive use, I recommend the use of Flyway or Liquibase.

For JPA, we also need a persistence.xml class in the src/main/resources/META-INF directory of the adapter module with the following content:

<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
  <persistence-unit name="eu.happycoders.shop.adapter.out.persistence.jpa">
    <class>eu.happycoders.shop.adapter.out.persistence.jpa.CartJpaEntity</class>
    <class>eu.happycoders.shop.adapter.out.persistence.jpa.CartLineItemJpaEntity</class>
    <class>eu.happycoders.shop.adapter.out.persistence.jpa.ProductJpaEntity</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
  </persistence-unit>
</persistence>
Code language: HTML, XML (xml)

Note that the name of the persistence unit in the XML file, “eu.happycoders.store.adapter.out.persistence.jpa”, is precisely the one passed as the first parameter to the Persistence.createEntityManagerFactory(…) method in the EntityManagerFactoryFactory.

Integration Tests for JPA Adapters

In the Unit Tests for Persistence Adapters section of the previous part of the series, I explained the division of the test classes into the abstract classes AbstractProductRepositoryTest and AbstractCartRepositoryTest and the concrete classes InMemoryProductRepositoryTest and InMemoryCartRepositoryTest: the abstract classes implement all test cases, and the concrete classes create the respective instances of the JPA adapter classes.

Here again is the corresponding class diagram:

Ports and Adapters: Hierarchy of JPA adapter test classes
Hierarchy of JPA adapter test classes

To test the two new JPA adapters, we just need to add the classes JpaProductRepositoryTest and JpaCartRepositoryTest and implement the abstract methods defined in the abstract base classes createProductRepository() and createCartRepository(), respectively.

Here is a screenshot of all adapter test classes in my IDE:

Ports and Adapters with Java – Adapter Test Classes

In the next section, I'll show you how to implement one of the two JPA repository adapters. The other adapter is implemented analogously.

Integration Tests for the JpaProductRepository Adapter

As a new dependency, we had previously only added the Jakarta Persistence API to the adapter module’s pom.xml file. To run the integration tests, we now also need Hibernate as a concrete implementation of this API.

To do this, we add the following dependencies in the test scope:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.3.1.Final</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>5.0.0-M1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>Code language: HTML, XML (xml)

hibernate-core is the actual hibernate library. It also needs hibernate-validator, jakarta.el, and a JDBC driver at runtime – as such, we use mysql-connector-java.

The last dependency, org.testcontainers:mysql, allows us, as you will see in a moment, to very conveniently launch a MySQL database from within the integration tests via Docker.

We then implement the JpaProductRepositoryTest as follows in the eu.happycoders.store.adapter.out.persistence.jpa package:

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)

In the method startDatabase() annotated with @BeforeAll, we start the MySQL database via the Testcontainers JDBC API in the first two lines.

Then, using our EntityManagerFactoryFactory created in the previous section, we make an EntityManagerFactory and save it in the entityManagerFactory field. The username and password are the default values of the test container MySQL module.

The createProductRepository() method overwritten from the abstract parent class then only has to create an instance of JpaProductRepository and pass the previously created EntityManagerFactory instance to its constructor.

In the stopDatabase() method annotated with @AfterAll, we close the EntityManagerFactory and stop the MySQL Docker container.

The test class JpaCartRepositoryTest is implemented analogously.

You should now be able to run the tests directly from your IDE or with the following call from a terminal:

mvn clean testCode language: plaintext (plaintext)

Adapting the Bootstrap Module

Our JPA adapters are ready. Now, we still have to adapt the bootstrap module so that – as announced in the introduction – we can specify via system property whether we want to use the old in-memory adapters or the new JPA adapters.

To do this, we must first add the following dependencies to the bootstrap module’s pom.xml. These are the same dependencies as we added in the adapter module in the test scope:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.3.1.Final</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>5.0.0-M1</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
    <scope>runtime</scope>
</dependency>Code language: HTML, XML (xml)

By the way, in the GitHub repository, I defined the versions centrally in a dependency management block in the pom.xml in the root directory, not in the pom.xml files of the modules. However, it would have been a bit confusing if I had made two entries for each dependency.

Unfortunately, there is still a small incompatibility at the moment: Both resteasy-undertow and hibernate-core have a dependency on the jandex library – but with two different group IDs, so Maven includes both libraries in the classpath. We can prevent this by adding an exclusion block to the existing dependency to resteasy-undertow:

<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>
Code language: HTML, XML (xml)

Finally, we need to adjust the initPersistenceAdapters() method of the RestEasyUndertowShopApplication.

Currently, this method looks as follows:

private void initPersistenceAdapters() {
  cartRepository = new InMemoryCartRepository();
  productRepository = new InMemoryProductRepository();
}
Code language: Java (java)

We amend it as follows:

private void initPersistenceAdapters() {
  String persistence = System.getProperty("persistence", "inmemory");
  switch (persistence) {
    case "inmemory" -> initInMemoryAdapters();
    case "mysql" -> initMySqlAdapters();
    default -> throw new IllegalArgumentException(
        "Invalid 'persistence' property: '%s' (allowed: 'inmemory', 'mysql')"
            .formatted(persistence));
  }
}

private void initInMemoryAdapters() {
  cartRepository = new InMemoryCartRepository();
  productRepository = new InMemoryProductRepository();
}

private void initMySqlAdapters() {
  EntityManagerFactory entityManagerFactory =
      EntityManagerFactoryFactory.createMySqlEntityManagerFactory(
          "jdbc:mysql://localhost:3306/shop", "root", "test");

  cartRepository = new JpaCartRepository(entityManagerFactory);
  productRepository = new JpaProductRepository(entityManagerFactory);
}Code language: Java (java)

We first read the system property “persistence,” providing the value “inmemory” as default. Depending on the value set, we then call either initInMemoryAdapters() or initMySqlAdapters().

The initInMemoryAdapters() method corresponds to the previous initPersistenceAdapters() method and creates the in-memory adapters.

The method initMySqlAdapters() creates – as we did in the integration tests – an EntityManagerFactory via the EntityManagerFactoryFactory and passes it to the constructors of JpaCartRepository and JpaProductRepository.

Connection URL, username, and password are hardcoded here – of course, we should not do this in a productive application.

Starting the Application

You can start the application again directly from your IDE by starting the Launcher class of the bootstrap module. If you do not change other settings, the application will continue to launch in in-memory mode.

Starting a Local MySQL Database

To start the application in MySQL mode, you must first start a local MySQL database. The easiest way to do this is via Docker, for example, using the following command:

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

If another MySQL installation already occupies your local port 3306, simply use another free port, e.g., port 3307, by replacing -p3306:3306 with -p3307:3306. You must then adjust the port in the initMySqlAdapters() method of the RestEasyUndertowShowApplication class accordingly.

Starting the Application in MySQL Mode

Finally, to start the application in MySQL mode, you must set the appropriate system property. In IntelliJ, for example, this is done as follows:

Click on the green start icon of the Launcher class and then on "Modify Run Configuration...":

Starting the Ports and Adapters application – changing the configuration

In the dialog that pops up, click “Modify options” and then “Add VM options” – or press Alt+V. That makes the “VM options” input field visible. Enter there “-Dersistence=mysql”:

Starting the ports and adapters application – setting the system property

Click “OK” and then start the application. I also like to enable the last option, “Show the run/debug configuration settings before start” in the “Modify options” dropdown. Then, the “Edit Run Configuration” window opens every time you start the application, so you can see the system property every time you start it and change it if necessary.

When the application runs, you can test its endpoints – as described in the “Starting the application” section of the previous part. Unlike the in-memory solution, all data should now be retained after a restart of the application.

Summary and Outlook

In this part of the tutorial, we have extended an application developed according to the ports-and-adapters architecture with a database adapter. In addition to implementing the entity, mapper, and adapter classes in the adapter module, we only needed to add a few lines of code in the bootstrap module to instantiate the new adapter classes.

In the core of the application, i.e., in the model and application modules, we did not have to change a single line of code – an impressive feature of the ports-and-adapters architecture!

Now we have a runnable application – but we have already found that generating the EntityManager and the transaction management required quite a bit of boilerplate code. And if we want to make our application “production-ready,” we would need to make it more configurable and robust, and we would also require health and metrics endpoints. Without an application framework, it would slowly become cumbersome.

Therefore, in the following two parts of the article series, I’ll show you how we convert our application into a Quarkus application and then to a Spring application. And with that, I will demonstrate – hopefully impressively – that with the ports-and-adapters architecture, even the application framework is just an interchangeable technical detail.

If you liked the article, I would be pleased about 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.