Advanced Practices in Spring Boot: Building a Modular Application with Docker, Zipkin, and 100% Code Coverage

Advanced practices in software development not only streamline production but also enhance the maintainability and scalability of applications. This blog post delves into constructing a Spring Boot application, utilizing Docker for consistent local environment, Zipkin for tracing, and strategies to achieve 100% code coverage. We’ll explore setting up a feature-based modular bookstore application as an example. We’ll leverage JPA for data persistence, Swagger for API documentation, Postgres as our database, Jacoco for code coverage, and Spring Modulith for documenting the application structure. These tools and practices are integral to building a robust, efficient, and scalable application.

Prerequisites

Before we dive into the development process, ensure you have:

  1. Java 21 installed on your system. You can install using sdkman, and select Java 21 https://sdkman.io/usage
  2. Docker and Docker Compose installed for setting up the local environment.

You can clone the https://github.com/dmakariev/examples repository.

git clone https://github.com/dmakariev/examples.git
cd examples/spring-boot/bookstore

Structure of the Application

In building complex systems, a well-structured approach is essential for managing scalability and maintainability. Our bookstore application is organized into several distinct modules, each meticulously designed to handle specific aspects of the business. This segmentation facilitates independent development and simplifies maintenance, enhancing overall system robustness.

High Level Structure

The bookstore application consists of

  • Inventory Module: This module is tasked with the management of stock levels and interactions with the book inventory, ensuring real-time updates on inventory changes are systematically processed. It serves as the backbone for tracking stock availability within the bookstore.
  • Notification Module: Designed to handle event processing, this module responds to events such as StockAddedEvent and StockRemovedEvent. It issues alerts to the system and notifies relevant stakeholders, thus facilitating effective communication across the application.
  • Order Module: This module manages all facets of order processing, from initial placement through to final fulfillment. It integrates seamlessly with the Inventory Module to ensure stock levels are accurately adjusted in real time, maintaining a balance between order demand and inventory supply.
  • Product Module: Responsible for maintaining comprehensive records of products, including detailed information on books and authors. This module provides critical data to other modules, ensuring consistency and accuracy across the platform.
  • User Module: Focused on all user interactions with the system, this module manages tasks ranging from authentication to profile management. It secures user information while ensuring a seamless and efficient user experience.

This structured modular approach not only streamlines development and debugging but also enhances the application’s adaptability to evolving business needs and technological advancements. Each module operates independently yet communicates effectively with others, ensuring a cohesive and robust application architecture. Below is a high-level diagram generated by the Spring Modulith integration with the Asciidoctor Maven plugin, illustrating the interconnections and structure of the application modules.

Structure

Folder Structure of the Application

The application is organized into a structured hierarchy to facilitate clear separation of concerns and maintainability:

├── Dockerfile
├── compose.yaml
├── lombok.config
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── asciidoc
    │   │   └── index.adoc
    │   ├── java
    │   │   └── com
    │   │       └── makariev
    │   │           └── examples
    │   │               └── spring
    │   │                   └── bookstore
    │   │                       ├── BookstoreApplication.java
    │   │                       ├── config
    │   │                       │   ├── GlobalRestControllerAdvice.java
    │   │                       │   └── SpringdocConfig.java
    │   │                       ├── inventory
    │   │                       │   ├── Inventory.java
    │   │                       │   ├── InventoryController.java
    │   │                       │   ├── InventoryRepository.java
    │   │                       │   ├── InventoryService.java
    │   │                       │   ├── StockAddedEvent.java
    │   │                       │   └── StockRemovedEvent.java
    │   │                       ├── notification
    │   │                       │   └── NotificationService.java
    │   │                       ├── order
    │   │                       │   ├── Order.java
    │   │                       │   ├── OrderController.java
    │   │                       │   ├── OrderItem.java
    │   │                       │   ├── OrderRepository.java
    │   │                       │   └── OrderService.java
    │   │                       ├── product
    │   │                       │   ├── Author.java
    │   │                       │   ├── AuthorController.java
    │   │                       │   ├── AuthorRepository.java
    │   │                       │   ├── AuthorService.java
    │   │                       │   ├── Book.java
    │   │                       │   ├── BookController.java
    │   │                       │   ├── BookRepository.java
    │   │                       │   ├── BookService.java
    │   │                       │   └── BookSummary.java
    │   │                       └── user
    │   │                           ├── Customer.java
    │   │                           ├── CustomerController.java
    │   │                           ├── User.java
    │   │                           ├── UserController.java
    │   │                           ├── UserRepository.java
    │   │                           └── UserService.java
    │   └── resources
    │       ├── application.properties
    │       ├── data.sql
    │       ├── schema.sql
    │       └── static
    │           └── index.html
    └── test
        ├── java
        │   └── com
        │       └── makariev
        │           └── examples
        │               └── spring
        │                   └── bookstore
        │                       ├── BookstoreApplicationTests.java
        │                       ├── ModularityTests.java
        │                       ├── inventory
        │                       │   ├── InventoryControllerIT.java
        │                       │   ├── InventoryControllerTest.java
        │                       │   ├── InventoryRepositoryTest.java
        │                       │   └── InventoryServiceTest.java
        │                       ├── order
        │                       │   ├── OrderControllerIT.java
        │                       │   ├── OrderControllerTest.java
        │                       │   ├── OrderRepositoryTest.java
        │                       │   └── OrderServiceTest.java
        │                       ├── product
        │                       │   ├── AuthorControllerIT.java
        │                       │   ├── AuthorControllerTest.java
        │                       │   ├── AuthorRepositoryTest.java
        │                       │   ├── AuthorServiceTest.java
        │                       │   ├── BookControllerIT.java
        │                       │   ├── BookControllerTest.java
        │                       │   ├── BookRepositoryTest.java
        │                       │   └── BookServiceTest.java
        │                       └── user
        │                           ├── CustomerControllerIT.java
        │                           ├── CustomerControllerTest.java
        │                           ├── UserControllerIT.java
        │                           ├── UserControllerTest.java
        │                           ├── UserRepositoryTest.java
        │                           └── UserServiceTest.java
        └── resources
            ├── application-integration-test.properties
            └── application-test.properties

Description of Key Folders and Files

  • /src/main/java/: Contains all the Java source files for the application, organized by module (inventory, notification, order, product, user). Each module includes its respective controllers, services, repositories, and entities.
  • /src/main/resources/: Houses configuration files like application.properties, database initialization scripts (data.sql and schema.sql), and static resources (index.html).
  • /src/test/: Includes all the test cases for the application, structured similarly to the main Java source directory. This includes unit tests, integration tests, and configuration files for test environments.
  • Dockerfile and compose.yaml: Define the Docker configuration for building the application image and setting up the multi-container environment with Docker Compose.
  • pom.xml: Maven configuration file that manages dependencies, plugins, and other configuration details necessary for building the application.
  • lombok.config: Configuration file for Lombok, a library that helps reduce boilerplate code in Java applications.

This folder structure supports a clean and organized development environment, facilitating easy navigation and maintenance of the application codebase. Each component is placed logically to enhance understandability and manageability, promoting best practices in software development.

Build the Application

Building our Spring Boot application involves several key steps and tools designed to streamline the development and testing process. At the core, the application leverages the Spring Boot Maven plugin, which simplifies the packaging and running of the application. For integration testing, we utilize the Maven Failsafe plugin, ensuring that our integration tests are conducted during the integration-test phase of the build lifecycle, which does not affect the earlier unit tests executed by the Surefire plugin.

Standard Build Commands

To manage the build lifecycle effectively, we use several Maven commands tailored for different stages of development:

Unit Testing: Execute all unit tests while skipping integration tests:

$ ./mvnw clean test

Packaging: Create an executable jar file without running integration tests:

$ ./mvnw clean package

Integration Testing: Include a thorough validation with integration tests:

$ ./mvnw verify

Full Build: Perform a complete build that incorporates documentation and code coverage analysis:

$ ./mvnw clean install -Pdocs,coverage

Running the Application

For development purposes, the application can be run directly using Spring Boot’s embedded server:

$ ./mvnw spring-boot:run

Alternatively, after building, the packaged application can be executed:

$ java -jar ./target/bookstore-0.0.1-SNAPSHOT.jar

You can also specify runtime configurations for different databases:

  • Postgres, it needs localhost instance of Postgres
    $ java -jar  \
     -Dspring.datasource.url=jdbc:postgresql://localhost:5432/example \
     -Dspring.datasource.username=postgres \
     -Dspring.datasource.password=postgres \
     -Dspring.jpa.hibernate.ddl-auto=update \
     ./target/bookstore-0.0.1-SNAPSHOT.jar
    
  • H2 Database in memory
    $ java -jar  \
     -Dspring.datasource.url=jdbc:h2:mem:example \
     ./target/bookstore-0.0.1-SNAPSHOT.jar
    
  • H2 Database filesystem - database data is stored in file
    $ java -jar  \
     -Dspring.datasource.url=jdbc:h2:file:./example-db-data \
     -Dspring.jpa.hibernate.ddl-auto=update \
     ./target/bookstore-0.0.1-SNAPSHOT.jar
    

Additional Maven Profiles

Docs Profile

The docs profile utilizes the asciidoctor-maven-plugin to generate comprehensive HTML documentation from AsciiDoc files found in src/main/asciidoc. This documentation includes detailed module descriptions, interactions, and automatically generated PlantUML diagrams to illustrate the application structure.

Output: The documentation is compiled into HTML and stored in ./target/generated-docs/index.html, providing an accessible and detailed overview of the application’s architecture.

Coverage Profile

The coverage profile, facilitated by the jacoco-maven-plugin, generates detailed reports on code coverage, ensuring extensive test coverage across unit and integration tests.

Output: The coverage reports are placed in ./target/site/jacoco/index.html, offering an interactive interface to review code coverage metrics and identify untested areas.

Coverage

Example Commands for Profiles Execution

$ ./mvnw clean install -Pdocs
$ ./mvnw clean install -Pcoverage
$ ./mvnw clean install -Pdocs,coverage

These commands ensure that our development process is not only efficient but also thorough, providing all necessary tools and reports to maintain high standards of quality and documentation. By integrating these practices, we enhance the robustness and readability of our Spring Boot application, making it easier to manage, scale, and understand.

Integrating Docker Compose in the Spring Boot Application

Our Spring Boot application leverages Docker Compose to orchestrate multiple services, ensuring they are deployed in a consistent and reliable environment. The use of Docker Compose facilitates the management of service dependencies and simplifies the configuration of services running in our local development environment. Below is a brief overview of how Docker Compose is configured and the services it manages:

Docker Compose Configuration

The docker-compose.yaml file defines several services, including the backend application, PostgreSQL database, pgAdmin for database management, and Zipkin for distributed tracing. Here’s how these services are orchestrated:

  • Backend Service: Configured to build from a Dockerfile, this service runs the bookstore backend on port 8088, which is mapped to port 8080 on the host. It connects to PostgreSQL using credentials and configurations specified in the environment variables. Database (db): Uses the official PostgreSQL image and stores data in a named volume to persist data across container restarts.
  • pgAdmin: Provides a web-based interface for database management, accessible through port 5050.
  • Zipkin: Offers tracing capabilities to monitor and troubleshoot requests across the distributed system, accessible via port 9411.

Accessing Services After Deployment

Once docker compose up --build is executed, it brings up all the configured services. Here are the URLs where these services can be accessed locally:

This Docker Compose setup not only ensures that each component of our application is correctly configured and interconnected but also provides a robust platform for development, testing, and monitoring the application’s performance in a simulated production environment.

Conclusion

In this blog post, we’ve explored the detailed setup and configuration of a feature-based Spring Boot application tailored to meet the complex requirements of modern applications and microservices. Emphasizing clean code and clean architecture, our approach promotes low coupling and high cohesion among components. Through the use of Docker Compose, we’ve streamlined the deployment of essential services such as PostgreSQL, pgAdmin, and Zipkin, enhancing our development and observability environment. Detailed documentation and high code coverage are not just ideal goals but practical outcomes, achieved through our diligent use of Maven profiles. This systematic approach not only boosts development efficiency but also equips developers with robust tools for monitoring and managing applications effectively. These practices exemplify the standards necessary for building scalable, maintainable, and high-quality software, ensuring that each component operates efficiently both independently and as part of the whole system.


Coffee Time!


Happy coding!

Share: Twitter LinkedIn