Campsite Booking API : Revisited 3

Another year passed, and I decided to return to this project again and implement a new batch of improvements. So, continuing the series “Campsite Booking API (Java)” with this installment, I detail changes to the project and its current state.

Before I started any development on this iteration, I came up with a list of changes, which I formalized using GitHub’s projects feature. To do so, I created the Campsite Booking 2023 project with all the items I wanted to implement. Creating such a project helped me clarify the scope of the new iteration, plan my work, and track progress. And now, let’s dive into the most significant changes.

Upgrade to Spring Boot 3

Since Spring Boot 3 was released almost a year ago, this project was well due for an upgrade from version 2 to version 3. Given that this project was already using Java 17 LTS, which is the main prerequisite for Spring Boot 3 migration, the upgrade consisted of bumping the version of the spring-boot-starter-parent artifact from 2.7.1 to 3.1.4. Also, given that Spring Boot 3 moved from Java EE to Jakarta EE APIs for all dependencies, I had to replace all javax imports with jakarta ones.

Check this commit for more details.

Entity Classes for DB Layer

This sample project is based on the layered architecture that consists of the presentation(BookingController.java class back then, later renamed to BookingApiControllerImpl.java), business(BookingService.java class), persistence(BookingRepository.java class), and database(MySQL or Derby DBs) layers. Each of the first three layers typically operates with its proper object classes, namely, data transfer object( DTO), model, and entity classes, respectively.

But back then, it was implemented so that the business and persistence layers operated on the same object class, namely Booking.java. So, to make all layers work on their proper object classes, I added an entity object class named BookingEntity.java for the persistence layer.

Check this commit for more details.

Explicit given-when-then Pattern for Tests

When introducing the entity object class for the persistence level, I had to update the corresponding unit and integration tests. During the previous iteration, I rewrote all unit and integration tests using JUnit 5 in BDD style, which turned out to be a wrong decision. It’s more challenging to make significant changes to the test methods when the code in it for the given, when, and then parts are encapsulated in methods prefixed with given_, when_, and then_ parts, respectively.

So, to solve this issue, I reworked all unit and integration tests again with an explicit given-when-then pattern using the following convention:

  • All tests related to a particular method should be encapsulated in a nested class annotated with the JUnit 5 @Nested annotation.
  • The instance variable for the class under test should be named classUnderTest.
  • Methods that test a happy path execution should be named happy_path. Methods that test other preconditions and inputs should be named given_<preconditions_and_inputs>__then_<expected_results>.
  • Code within a test method should be laid out per the given-when-then pattern with the explicit // given, // when, and // then comments to improve readability and facilitate visualization of the corresponding code blocks.
  • The when part should only contain the invocation of the method under the test and the variable to which the result of this invocation is assigned should be named result.

Also, the BDD style does not play well with the parallel execution of tests since it forces the use of test class instance variables shared between the test methods.

Check this commit for more details.

Test Containers for Integration Tests

Previously, to implement integration tests, I used the Apache Derby database. But recently, I discovered a better alternative to it: a MySQL test container, which is part of the Testcontainers open-source framework for providing lightweight instances of almost anything that can run in a Docker container.

To implement the migration to the MySQL test container, firstly, I had to add two new dependencies to the pom.xml file:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>testcontainers</artifactId>
  <version>1.19.1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mysql</artifactId>
  <version>1.19.1</version>
  <scope>test</scope>
</dependency>

Secondly, I updated the BaseIT.java class by adding the container and corresponding Spring data source properties initializations:

@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("mysql")
public abstract class BaseIT {

  private static final String MYSQL_DOCKER_IMAGE_NAME = "mysql:8-debian";
  private static final String MYSQL_DATABASE_NAME = "test_campsite";

  static final MySQLContainer<?> mySqlContainer;
  static {
    mySqlContainer =
        new MySQLContainer<>(MYSQL_DOCKER_IMAGE_NAME).withDatabaseName(MYSQL_DATABASE_NAME);
    mySqlContainer.start();
  }

  @DynamicPropertySource
  static void mysqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl);
    registry.add("spring.datasource.username", mySqlContainer::getUsername);
    registry.add("spring.datasource.password", mySqlContainer::getPassword);
    registry.add("spring.jpa.properties.hibernate.show_sql", () -> "true");
  }
}

Check this commit for more details.

Flyway Migrations

Another enhancement implemented is the database migrations or versioning using Flyway, an open-source database migration tool. Previously, the database schema was created or updated on every application initialization using Spring Data JPA’s built-in schema generation feature by defining the spring.jpa.hibernate.ddl-auto property in the application properties file.

So, to implement this feature, I started by adding two new dependencies to the pom.xml file:

<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-core</artifactId>
  <version>9.22.3</version>
</dependency>
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-mysql</artifactId>
  <version>9.22.3</version>
</dependency>

Then, while removing the spring.jpa.hibernate.ddl-auto property in the application properties file, I added two new DDL SQL migration scripts for MySQL and Apache Derby vendors, which were placed into the default location for Flyway scripts, namely, the db/migration folder:

Flyway Migration Scripts Location

Also, since I provided migration scripts for two vendors, I had to define the spring.flyway.locations application property as follows:

spring.flyway.locations=classpath:db/migration/{vendor}

Depending on the data source used, the {vendor} placeholder will be replaced automatically with either mysql or derby value on application start.

Check this commit for more details.

Multi-module Maven Project

Before implementing an API-first design approach, I had to migrate this project to Maven’s multi-module project since, in the end, I should have ended up with two submodules, one for the API contract and another for the actual implementation of the API, something like below:

├── campsite-booking
│   ├── campsite-booking-api
│   ├── campsite-booking-service

So, at this stage, I only extracted the current API implementation into the campsite-booking-service submodule while completely reworking the parent POM. Switching to a multi-module project required certain adjustments in the Dockerfile. Previously, the following command was used to build the application’s executable JAR:

RUN mvn --batch-mode package -DskipTests -DskipITs; \
    mv /usr/src/app/target/campsite-booking-$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout).jar \
        /usr/src/app/target/app.jar

With the existing campsite-booking-service submodule, the above command had to be updated by appending spring-boot:repackage to the mvn package command and adjusting the path to the produced JAR file.

RUN mvn package spring-boot:repackage --batch-mode -DskipTests -DskipITs; \
    mv /usr/src/app/campsite-booking-service/target/campsite-booking-service-$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout).jar \
        /usr/src/app/app.jar

The spring-boot:repackage command is needed here to ensure that the repackaged application’s JAR contains not only compiled Java classes from the submodule but also all the required runtime dependencies. Also, it was necessary to define the start-class property in the submodule’s POM:

<properties>
    <start-class>com.kiroule.campsitebooking.CampsiteBookingServiceApp</start-class>
</properties>

Check this commit for more details.

API-first Design Approach

And here comes the final change, or the culmination of this improvements’ iteration, namely, the migration to the API-first design approach. To learn more about what it means and what its benefits are, I recommend reading this article.

This migration consisted of three main blocks. The first consisted of creating an API design document using the OpenAPI Specification. The campsite-booking-service module has already been implemented using the OpenAPI specification but using an implementation-first approach. So, given, for example, that the service is running in your local dev, the initial API design document can be obtained from this URL: http://localhost:8080/v3/api-docs.yaml. Next, I had to make certain adjustments to it, and the final version of the campsite-booking-api.yaml file can be seen here.

The second part was to add a new submodule named campsite-booking-api that contained the newly created campsite-booking-api.yaml design document. Also, the pom.xml of this submodule had to be updated to enable the generation of DTO and interface Java classes from the API design document. To do so, I used the openapi-generator-maven-plugin build plugin, which, when executed, generates the following classes:

OpenAPI Generator Generated Sources

Third, the campsite-booking-service submodule was modified by adding a new dependency for the com.kiroule.campsite-booking-api artifact, and based on the generated code, certain adjustments were made to the BookingController.java class, which was renamed to BookingApiControllerImpl.java.

Check this commit for more details.

Continue reading the series “Campsite Booking API (Java)”: