Quantcast
Channel: Piotr's TechBlog
Viewing all articles
Browse latest Browse all 47

Guide to Modulith with Spring Boot

$
0
0

This article will teach you how to build modulith with Spring Boot and use the Spring Modulith project features. Modulith is a software architecture pattern that assumes organizing your monolith app into logical modules. Such modules should be independent of each other as much as possible. Modulith balances monolithic and microservices-based architectures. It can be your target model for organizing the app. But you can also treat it just as a transitional phase during migration from the monolithic into a microservices-based approach. Spring Modulith will help us build a well-structured Spring Boot app and verify dependencies between the logical modules.

We will compare the current approach with the microservices-based architecture. In order to do that, we implement very similar functionality as described in my latest article about building microservices with Spring Cloud and Spring Boot 3.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions.

Before we start, let’s take a look at the following diagram. It illustrates the architecture of our sample system. We have three independent modules, which communicate with each other: employee, department, and organization. There is also the gateway module. It is responsible for exposing internal services as the REST endpoints outside of the app. Our modules send traces to the Zipkin instance using the support provided within the Spring Modulith project.

spring-boot-modulith-arch

If you want to compare it with the similar microservices architecture described in the previously mentioned article here’s the diagram.

Let’s take a look at the structure of our code. By default, each direct sub-package of the main package is considered an application module package. So there are four application modules: department, employee, gateway, and organization. Each module contains “provided interfaces” exposed to the other modules. We need to place them in the application module root directory. Other modules cannot access any classes or beans from application module sub-packages. We will discuss it in detail in the next sections.

src/main/java
└── pl
    └── piomin
        └── services
            ├── OrganizationAddEvent.java
            ├── OrganizationRemoveEvent.java
            ├── SpringModulith.java
            ├── department
            │   ├── DepartmentDTO.java
            │   ├── DepartmentExternalAPI.java
            │   ├── DepartmentInternalAPI.java
            │   ├── management
            │   │   ├── DepartmentManagement.java
            │   │   └── package-info.java
            │   ├── mapper
            │   │   └── DepartmentMapper.java
            │   ├── model
            │   │   └── Department.java
            │   └── repository
            │       └── DepartmentRepository.java
            ├── employee
            │   ├── EmployeeDTO.java
            │   ├── EmployeeExternalAPI.java
            │   ├── EmployeeInternalAPI.java
            │   ├── management
            │   │   └── EmployeeManagement.java
            │   ├── mapper
            │   │   └── EmployeeMapper.java
            │   ├── model
            │   │   └── Employee.java
            │   └── repository
            │       └── EmployeeRepository.java
            ├── gateway
            │   └── GatewayManagement.java
            └── organization
                ├── OrganizationDTO.java
                ├── OrganizationExternalAPI.java
                ├── management
                │   └── OrganizationManagement.java
                ├── mapper
                │   └── OrganizationMapper.java
                ├── model
                │   └── Organization.java
                └── repository
                    └── OrganizationRepository.java

Dependencies

Let’s take a look at a list of required dependencies. Our app exposes some REST endpoints and connects to the embedded H2 database. So, we need to include Spring Web and Spring Data JPA projects. In order to use Spring Modulith, we have to add the spring-modulith-starter-core starter. We will also do some mappings between entities and DTO classes. Therefore we will include the mapstruct project that simplifies mappings between Java beans.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>1.5.5.Final</version>
</dependency>

The Structure of Application Modules

We will analyze the structure of our modules on the example of the employee module. All the interfaces/classes in the module root directory can be called from other modules (green color). Other modules cannot call any interfaces/classes from module sub-packages (red color).

spring-boot-modulith-code-structure

The app implementation is not complicated. Here’s our Employee entity class:

@Entity
public class Employee {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   private Long organizationId;
   private Long departmentId;
   private String name;
   private int age;
   private String position;

   // ... GETTERS/SETTERS
}

We are using the Spring Data JPA Repository pattern to interact with the H2 database. Instead of the entity classes, we are returning the DTO objects using the Spring Data projection feature.

public interface EmployeeRepository extends CrudRepository<Employee, Long> {
   List<EmployeeDTO> findByDepartmentId(Long departmentId);
   List<EmployeeDTO> findByOrganizationId(Long organizationId);
   void deleteByOrganizationId(Long organizationId);
}

Here’s our DTO record. It is exposed outside the module because other modules will have to access Employee data. We don’t want to expose entity class directly, so DTO is a very useful pattern here.

public record EmployeeDTO(Long id,
                          Long organizationId,
                          Long departmentId,
                          String name,
                          int age,
                          String position) {
}

Let’s also define a mapper between entity and DTO using the mapstruct support.

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface EmployeeMapper {
    EmployeeDTO employeeToEmployeeDTO(Employee employee);
    Employee employeeDTOToEmployee(EmployeeDTO employeeDTO);
}

We want to hide the implementation details of the main module @Service behind other modules. Therefore we will expose the required methods via the interface. Other modules will use the interface to call the @Service methods. The InternalAPI suffix means that this interface is just for internal usage between the modules.

public interface EmployeeInternalAPI {

   List<EmployeeDTO> getEmployeesByDepartmentId(Long id);
   List<EmployeeDTO> getEmployeesByOrganizationId(Long id);

}

In order to expose some @Service methods outside the app as the REST endpoints we will use the ExternalAPI suffix in the interface name. For the employee module, we only expose the method for adding new employees.

public interface EmployeeExternalAPI {
   EmployeeDTO add(EmployeeDTO employee);
}

Our management @Service implements both external and internal interfaces. It injects and uses the repository and mapper beans. here are two internal methods used by the department and organization modules (1), a single external method exposed as the REST endpoint (2), and the method for processing asynchronous events from other modules (3). We will discuss the last one of the methods later.

@Service
public class EmployeeManagement implements EmployeeInternalAPI, 
                                           EmployeeExternalAPI {

   private static final Logger LOG = LoggerFactory
      .getLogger(EmployeeManagement.class);
   private EmployeeRepository repository;
   private EmployeeMapper mapper;

   public EmployeeManagement(EmployeeRepository repository,
                             EmployeeMapper mapper) {
      this.repository = repository;
      this.mapper = mapper;
   }

   @Override // (1)
   public List<EmployeeDTO> getEmployeesByDepartmentId(Long departmentId) {
      return repository.findByDepartmentId(departmentId);
   }

   @Override // (1)
   public List<EmployeeDTO> getEmployeesByOrganizationId(Long id) {
      return repository.findByOrganizationId(id);
   }

   @Override
   @Transactional // (2)
   public EmployeeDTO add(EmployeeDTO employee) {
      Employee emp = mapper.employeeDTOToEmployee(employee);
      return mapper.employeeToEmployeeDTO(repository.save(emp));
   }

   @ApplicationModuleListener // (3)
   void onRemovedOrganizationEvent(OrganizationRemoveEvent event) {
      LOG.info("onRemovedOrganizationEvent(orgId={})", event.getId());
      repository.deleteByOrganizationId(event.getId());
   }

}

Verify Dependencies with Spring Modulith

Let’s switch to the department module. It needs to access data exposed by the employee module. In order to do that, it will use the methods provided within the EmployeeInternalAPI interface. The implementation in the form of the EmployeeManagement class should be hidden from the department module. However, let’s imagine that the department module calls the EmployeeManagement bean directly. Here’s the fragment of the DepartmentManagement implementation:

@Service
public class DepartmentManagement {

   private DepartmentRepository repository;
   private EmployeeManagement employeeManagement;
   private DepartmentMapper mapper;

   public DepartmentManagement(DepartmentRepository repository,
                               EmployeeManagement employeeManagement,
                               DepartmentMapper mapper) {
      this.repository = repository;
      this.employeeManagement = employeeManagement;
      this.mapper = mapper;
   }

   public DepartmentDTO getDepartmentByIdWithEmployees(Long id) {
      DepartmentDTO d = repository.findDTOById(id);
      List<EmployeeDTO> dtos = employeeManagement
         .getEmployeesByDepartmentId(id);
      d.employees().addAll(dtos);
      return d;
   }
}

Here comes the Spring Modulith project. For example, we can create the JUnit test to verify the dependencies between app modules. Once we break the modulith rules our test will fail. Let’s see how can leverage the verify() method provided by the Spring Modulith ApplicationModules class:

public class SpringModulithTests {

   ApplicationModules modules = ApplicationModules.of(SpringModulith.class);

   @Test
   void shouldBeCompliant() {
      modules.verify();
   }
}

Here’s the verification result for the previously introduced implementation of DepartmentManagement bean:

Now, let’s do it in the proper way. First of all, the DepartmentDTO record uses the EmployeeDTO. This relation is represented only at the DTO level.

public record DepartmentDTO(Long id,
                            Long organizationId,
                            String name,
                            List<EmployeeDTO> employees) {
    public DepartmentDTO(Long id, Long organizationId, String name) {
        this(id, organizationId, name, new ArrayList<>());
    }
}

There are no relations between the entities and corresponding database tables. We want our modules to be independent at the database level. Although there are no relations between the tables, we still use a single database. Here’s the DepartmentEntity class:

@Entity
public class Department {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   private Long organizationId;
   private String name;

   // ... GETTERS/SETTERS
}

The same as before, there is a mapper to convert between the entity and DTO:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface DepartmentMapper {
   DepartmentDTO departmentToEmployeeDTO(Department department);
   Department departmentDTOToEmployee(DepartmentDTO departmentDTO);
}

Here’s the repository interface:

public interface DepartmentRepository extends CrudRepository<Department, Long> {

   @Query("""
          SELECT new pl.piomin.services.department.DepartmentDTO(d.id, d.organizationId, d.name)
          FROM Department d
          WHERE d.id = :id
          """)
   DepartmentDTO findDTOById(Long id);

   @Query("""
          SELECT new pl.piomin.services.department.DepartmentDTO(d.id, d.organizationId, d.name)
          FROM Department d
          WHERE d.organizationId = :organizationId
          """)
   List<DepartmentDTO> findByOrganizationId(Long organizationId);

   void deleteByOrganizationId(Long organizationId);
}

The department module calls methods exposed by the employee module, but it also provides methods for the organization module. Once again we are creating the *InternalAPI interface.

public interface DepartmentInternalAPI {
    List<DepartmentDTO> getDepartmentsByOrganizationId(Long id);
}

Here’s the interface with the methods exposed outside the app as the REST endpoints.

public interface DepartmentExternalAPI {
    DepartmentDTO getDepartmentByIdWithEmployees(Long id);
    DepartmentDTO add(DepartmentDTO department);
}

Finally, we can implement the DepartmentManagement bean. Once again, it contains a method for synchronous calls and two methods for processing events asynchronously (annotated with @ApplicationModuleListener).

@Service
public class DepartmentManagement implements DepartmentInternalAPI, DepartmentExternalAPI {

   private static final Logger LOG = LoggerFactory
      .getLogger(DepartmentManagement.class);
   private DepartmentRepository repository;
   private EmployeeInternalAPI employeeInternalAPI;
   private DepartmentMapper mapper;

   public DepartmentManagement(DepartmentRepository repository,
                               EmployeeInternalAPI employeeInternalAPI,
                               DepartmentMapper mapper) {
      this.repository = repository;
      this.employeeInternalAPI = employeeInternalAPI;
      this.mapper = mapper;
   }

   @Override
   public DepartmentDTO getDepartmentByIdWithEmployees(Long id) {
      DepartmentDTO d = repository.findDTOById(id);
      List<EmployeeDTO> dtos = employeeInternalAPI
         .getEmployeesByDepartmentId(id);
      d.employees().addAll(dtos);
      return d;
   }

   @ApplicationModuleListener
   void onNewOrganizationEvent(OrganizationAddEvent event) {
      LOG.info("onNewOrganizationEvent(orgId={})", event.getId());
      add(new DepartmentDTO(null, event.getId(), "HR"));
      add(new DepartmentDTO(null, event.getId(), "Management"));
   }

   @ApplicationModuleListener
   void onRemovedOrganizationEvent(OrganizationRemoveEvent event) {
      LOG.info("onRemovedOrganizationEvent(orgId={})", event.getId());
      repository.deleteByOrganizationId(event.getId());
   }

   @Override
   public DepartmentDTO add(DepartmentDTO department) {
      return mapper.departmentToEmployeeDTO(
         repository.save(mapper.departmentDTOToEmployee(department))
      );
   }

   @Override
   public List<DepartmentDTO> getDepartmentsByOrganizationId(Long id) {
      return repository.findByOrganizationId(id);
   }
}

Processing Asynchronous Events

Until now we discussed the synchronous communication between the application modules. It is usually the most common way of communication we need. However, in some cases, we can rely on asynchronous events exchanged between the modules. There is support for such an approach in Spring Boot and Spring Modulith. It is based on the Spring ApplicationEvent mechanism.

Let’s switch to the organization module. In the OrganizationManagement module we are implementing several synchronous operations, but we are also sending some Spring events using the ApplicationEventPublisher bean (1). Those events are propagated after adding (2) and removing (3) the organization. For example, assuming we will delete the organization we should also remove all the departments and employees. We can process those actions asynchronously on the department and employee modules side. Our event object contains the id of the organization.

@Service
public class OrganizationManagement implements OrganizationExternalAPI {

   private final ApplicationEventPublisher events; // (1)
   private final OrganizationRepository repository;
   private final DepartmentInternalAPI departmentInternalAPI;
   private final EmployeeInternalAPI employeeInternalAPI;
   private final OrganizationMapper mapper;

   public OrganizationManagement(ApplicationEventPublisher events,
                                 OrganizationRepository repository,
                                 DepartmentInternalAPI departmentInternalAPI,
                                 EmployeeInternalAPI employeeInternalAPI,
                                 OrganizationMapper mapper) {
      this.events = events;
      this.repository = repository;
      this.departmentInternalAPI = departmentInternalAPI;
      this.employeeInternalAPI = employeeInternalAPI;
      this.mapper = mapper;
   }

   @Override
   public OrganizationDTO findByIdWithEmployees(Long id) {
      OrganizationDTO dto = repository.findDTOById(id);
      List<EmployeeDTO> dtos = employeeInternalAPI.getEmployeesByOrganizationId(id);
      dto.employees().addAll(dtos);
      return dto;
   }

   @Override
   public OrganizationDTO findByIdWithDepartments(Long id) {
      OrganizationDTO dto = repository.findDTOById(id);
      List<DepartmentDTO> dtos = departmentInternalAPI.getDepartmentsByOrganizationId(id);
      dto.departments().addAll(dtos);
      return dto;
   }

   @Override
   public OrganizationDTO findByIdWithDepartmentsAndEmployees(Long id) {
      OrganizationDTO dto = repository.findDTOById(id);
      List<DepartmentDTO> dtos = departmentInternalAPI.getDepartmentsByOrganizationIdWithEmployees(id);
      dto.departments().addAll(dtos);
      return dto;
   }

   @Override
   @Transactional
   public OrganizationDTO add(OrganizationDTO organization) {
      OrganizationDTO dto = mapper.organizationToOrganizationDTO(
          repository.save(mapper.organizationDTOToOrganization(organization))
      );
      events.publishEvent(new OrganizationAddEvent(dto.id())); // (2)
      return dto;
   }

   @Override
   @Transactional
   public void remove(Long id) {
      repository.deleteById(id);
      events.publishEvent(new OrganizationRemoveEvent(id)); // (3)
   }

}

Then, the application events may be received by other modules. In order to handle the event we can use the @ApplicationModuleListener annotation provided by Spring Modulith. It is the shortcut for three different Spring annotations: @Async, @Transactional, and @TransactionalEventListener. In the fragment of the DepartmentManagement code, we are handling the incoming events. For the newly created organization, we are adding two default departments. After removing the organization we are removing all the departments previously assigned to that organization.

@ApplicationModuleListener
void onNewOrganizationEvent(OrganizationAddEvent event) {
   LOG.info("onNewOrganizationEvent(orgId={})", event.getId());
   add(new DepartmentDTO(null, event.getId(), "HR"));
   add(new DepartmentDTO(null, event.getId(), "Management"));
}

@ApplicationModuleListener
void onRemovedOrganizationEvent(OrganizationRemoveEvent event) {
   LOG.info("onRemovedOrganizationEvent(orgId={})", event.getId());
   repository.deleteByOrganizationId(event.getId());
}

There’s also a similar method for handling OrganizationRemoveEvent in the EmployeeDepartment.

@ApplicationModuleListener
void onRemovedOrganizationEvent(OrganizationRemoveEvent event) {
   LOG.info("onRemovedOrganizationEvent(orgId={})", event.getId());
   repository.deleteByOrganizationId(event.getId());
}

Spring Modulith comes with a smart mechanism for testing event processing. We are creating a test for the particular module by placing it in the right package. For example, it is the pl.piomin.services.department package to test the department module. We need to annotate the test class with @ApplicationModuleTest. There are three different bootstrap mode types available: STANDALONE, DIRECT_DEPENDENCIES and ALL_DEPENDENCIES. Spring Modulith provides the Scenario abstraction. It can be declared as a test method parameter in the @ApplicationModuleTest tests. Thanks to that object we can define a scenario to publish an event and verify the result in a single line of code.

@ApplicationModuleTest(ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DepartmentModuleTests {

    private static final long TEST_ID = 100;

    @Autowired
    DepartmentRepository repository;

    @Test
    @Order(1)
    void shouldAddDepartmentsOnEvent(Scenario scenario) {
        scenario.publish(new OrganizationAddEvent(TEST_ID))
                .andWaitForStateChange(() -> repository.findByOrganizationId(TEST_ID))
                .andVerify(result -> {assert !result.isEmpty();});
    }

    @Test
    @Order(2)
    void shouldRemoveDepartmentsOnEvent(Scenario scenario) {
        scenario.publish(new OrganizationRemoveEvent(TEST_ID))
                .andWaitForStateChange(() -> repository.findByOrganizationId(TEST_ID))
                .andVerify(result -> {assert result.isEmpty();});
    }
}

Exposing Modules API Externally with REST

Finally, let’s switch to the last module in our app – gateway. It doesn’t do much. It is responsible only for exposing some module services outside of the app using REST endpoints. In the first step, we need to inject all the *ExternalAPI beans.

@RestController
@RequestMapping("/api")
public class GatewayManagement {

   private DepartmentExternalAPI departmentExternalAPI;
   private EmployeeExternalAPI employeeExternalAPI;
   private OrganizationExternalAPI organizationExternalAPI;

   public GatewayManagement(DepartmentExternalAPI departmentExternalAPI,
                            EmployeeExternalAPI employeeExternalAPI,
                            OrganizationExternalAPI organizationExternalAPI) {
      this.departmentExternalAPI = departmentExternalAPI;
      this.employeeExternalAPI = employeeExternalAPI;
      this.organizationExternalAPI = organizationExternalAPI;
   }


   @GetMapping("/organizations/{id}/with-departments")
   public OrganizationDTO apiOrganizationWithDepartments(@PathVariable("id") Long id) {
        return organizationExternalAPI.findByIdWithDepartments(id);
   }

   @GetMapping("/organizations/{id}/with-departments-and-employees")
   public OrganizationDTO apiOrganizationWithDepartmentsAndEmployees(@PathVariable("id") Long id) {
      return organizationExternalAPI.findByIdWithDepartmentsAndEmployees(id);
   }

   @PostMapping("/organizations")
   public OrganizationDTO apiAddOrganization(@RequestBody OrganizationDTO o) {
      return organizationExternalAPI.add(o);
   }

   @PostMapping("/employees")
   public EmployeeDTO apiAddEmployee(@RequestBody EmployeeDTO employee) {
      return employeeExternalAPI.add(employee);
   }

   @GetMapping("/departments/{id}/with-employees")
   public DepartmentDTO apiDepartmentWithEmployees(@PathVariable("id") Long id) {
      return departmentExternalAPI.getDepartmentByIdWithEmployees(id);
   }

   @PostMapping("/departments")
   public DepartmentDTO apiAddDepartment(@RequestBody DepartmentDTO department) {
      return departmentExternalAPI.add(department);
   }
}

We can document the REST API exposed by our app using the Springdoc project. Let’s include the following dependency into Maven pom.xml:

<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
   <version>2.2.0</version>
</dependency>

Once we start the app with the mvn spring-boot:run command, we can access Swagger UI with our API documentation under the http://localhost:8080/swagger-ui.html address.

spring-boot-modulith-api

In order to ensure that everything works fine we can implement some REST-based Spring Boot tests. We don’t use any specific Spring Modulith support here, just Spring Boot Test features.

@SpringBootTest(webEnvironment = 
                SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AppRestControllerTests {

   @Autowired
   TestRestTemplate restTemplate;

   @Test
   @Order(1)
   void shouldAddNewEmployee() {
      EmployeeDTO emp = new EmployeeDTO(null, 1L, 1L, "Test", 30, "HR");
      emp = restTemplate.postForObject("/api/employees", emp, 
                                       EmployeeDTO.class);
      assertNotNull(emp.id());
   }

   @Test
   @Order(1)
   void shouldAddNewDepartment() {
      DepartmentDTO dep = new DepartmentDTO(null, 1L, "Test");
      dep = restTemplate.postForObject("/api/departments", dep, 
                                       DepartmentDTO.class);
      assertNotNull(dep.id());
   }

   @Test
   @Order(2)
   void shouldFindDepartmentWithEmployees() {
      DepartmentDTO dep = restTemplate
         .getForObject("/api/departments/{id}/with-employees",                    
                       DepartmentDTO.class, 1L);
      assertNotNull(dep);
      assertNotNull(dep.id());
   }
}

Here’s the result of the test visible above:

Documentation and Spring Boot Actuator Support in Spring Modulith

Spring Modulith provides an additional Actuator endpoint that shows the modular structure of the Spring Boot app. We include the following Maven dependencies to use that support:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.modulith</groupId>
   <artifactId>spring-modulith-actuator</artifactId>
   <scope>runtime</scope>
</dependency>

Then, let’s expose all the Actuator endpoints over HTTP by adding the following property to the application.yml file:

management.endpoints.web.exposure.include: "*"

Finally, we can call the modulith endpoint available under the http://localhost:8080/actuator/modulith address. Here’s the JSON response:

{
   "department" : {
      "basePackage" : "pl.piomin.services.department",
      "dependencies" : [
         {
            "target" : "employee",
            "types" : [
               "USES_COMPONENT"
            ]
         }
      ],
      "displayName" : "Department"
   },
   "employee" : {
      "basePackage" : "pl.piomin.services.employee",
      "dependencies" : [],
      "displayName" : "Employee"
   },
   "gateway" : {
      "basePackage" : "pl.piomin.services.gateway",
      "dependencies" : [
         {
            "target" : "employee",
            "types" : [
               "USES_COMPONENT"
            ]
         },
         {
            "target" : "department",
            "types" : [
               "USES_COMPONENT"
            ]
         },
         {
            "target" : "organization",
            "types" : [
               "USES_COMPONENT"
            ]
         }
      ],
      "displayName" : "Gateway"
   },
   "organization" : {
      "basePackage" : "pl.piomin.services.organization",
      "dependencies" : [
         {
            "target" : "employee",
            "types" : [
               "USES_COMPONENT"
            ]
         },
         {
            "target" : "department",
            "types" : [
               "USES_COMPONENT"
            ]
         }
      ],
      "displayName" : "Organization"
   }
}

If you prefer a more graphical form of docs we can leverage the Spring Modulith Documenter component. We don’t need to include anything, but just prepare a simple test that creates and customizes the Documenter object:

public class SpringModulithTests {

   ApplicationModules modules = ApplicationModules
      .of(SpringModulith.class);

   @Test
   void writeDocumentationSnippets() {
      new Documenter(modules)
             .writeModuleCanvases()
             .writeModulesAsPlantUml()
             .writeIndividualModulesAsPlantUml();
   }
}

Once we run the test Spring Modulith will generate the documentation files under the target/spring-modulith-docs directory. Let’s take a look at the UML diagram of our app modules.

Enable Observability

We can enable observability with the Micrometer between our application modules. The Spring Boot app will send them to Zipkin after setting the following list of dependencies:

<dependency>
   <groupId>org.springframework.modulith</groupId>
   <artifactId>spring-modulith-observability</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>io.micrometer</groupId>
   <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
   <groupId>io.opentelemetry</groupId>
   <artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

We can also change the default sampling level to 1.0 (100% of traces).

management.tracing.sampling.probability: 1.0

We can use Spring Boot support for Docker Compose to start Zipkin together with the app. First, let’s create the docker-compose.yml file in the project root directory.

version: "3.7"
services:
  zipkin:
    container_name: zipkin
    image: openzipkin/zipkin
    extra_hosts: [ 'host.docker.internal:host-gateway' ]
    ports:
      - "9411:9411"

Then, we need to add the following dependency in Maven pom.xml:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-docker-compose</artifactId>
</dependency>

Once, we start the app Spring Boot will try to run the Zipkin container on the Docker. In order to access the Zipkin dashboard visit the http://localhost:9411 address. You will see the traces visualization between the application modules. It works fine for asynchronous events communication. Unfortunately, it doesn’t visualize the synchronous communication properly, but maybe I did something wrong, or we need to wait for some improvements in the Spring Modulith project.

The post Guide to Modulith with Spring Boot appeared first on Piotr's TechBlog.


Viewing all articles
Browse latest Browse all 47

Trending Articles