Guide to Modulith with Spring Boot

Guide to Modulith with Spring Boot

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.

26 COMMENTS

comments user
Sebastian

Dzięki Piotrku za ten artykuł, śledziłem ten projekt od jakiegoś czasu w celu wykorzystania w następnym projekcie i po tej lekturze na pewno będzie mi dużo łatwiej.

Z tego co się orientuję to Modulith oferował także wsparcie jdbc w celu zapisywania eventów w bazie danych, może zainteresuje Cię ten temat, chętnie przeczytałbym o możliwych zastosowaniach tego mechanizmu w powyższej architekturze

    comments user
    piotr.minkowski

    Tak Spring Modulith oferuje takie możliwości. Ale jakoś do końca nie jestem do tego przekonany… W sensie nie wiem czy to jest tylko do utrwalenia eventu czy do jakiejś jego potencjalnej propagacji. A wtedy z kolei raczej widziałbym tu Kafkę, a nie bazy danych.

comments user
Belki

If they would add JPMS, then it would be interesting because of compile time checks and ability to distinguish public vs published types. But instead it looks like solving the problem that is artificially created and forces specific project structure
In case of spring it is easier to just put everything in single package and have at least control over types access level. With modulith you pretend to have some type of architecture, but instead just getting another problem to care about

    comments user
    piotr.minkowski

    Yes. However, for me it could be just a temporar phase before migrating to microservices. Anyway, adding JPMS could also be a good idea.

comments user
Zart colwing

Nice article. I would’ve conceal the DTOs into the externalAPIs, making the repositories return domain objects and letting only the gateway performs the mapping.

Also, the methods in the DepartmentMapper are badly named.

    comments user
    piotr.minkowski

    Thanks. But there is also an internal communication between modules performed not through the gateway. I don’t want to expose entity directly, since not all the fields should be visible for other modules.
    If you have any suggestions how to rename the methods, please make a PR in the repo.

comments user
Miklos Szots

Great article!

It would make sense to mention the publication registry shipped with modulith that does a bit more than the default application event mechanism. https://docs.spring.io/spring-modulith/reference/events.html#publication-registry

Still, these events won’t propagate across instances and no guarantee on event ordering might surprise the developers. Maybe this kind of eventing mechanism should only be used for idempotent / cleanup events, like cache busting or if it’s ok that some events might get lost.

    comments user
    piotr.minkowski

    Thanks.

    Yes, they won’t propage across the instances, biut the question is if it is any reason they should be propagated?

comments user
Mykhailo Skliar

Incredible

    comments user
    piotr.minkowski

    Thanks!

comments user
Mykhailo Skliar

Incredible!
How does Spring Data JPA converts from Employee Entity to EmployeeDTO ?
Does it use mapstruct mapper behind the scenes, or it just matches properties using reflection?

    comments user
    piotr.minkowski

    Thanks. It’s a built-in feature. Spring Data Projection

comments user
vamsi vegi

great write up, I want to develop a platform using this as a base with predefined interfaces that the end user can write some implementation in the principles of Modulith

kind of like plugins

so now the platform will scan a pre-defined folder and bring in the jar files as part of the overall execution.

How to do that

    comments user
    piotr.minkowski

    I’m not sure I understand your idea. Could describe it in more details?

comments user
Jesus Torres

Hi. Thanks for this post. I was trying to replicate this exercise and I had an error with the mapper:

Description:
Parameter 1 of constructor in pl.piomin.services.employee.management.EmployeeManagement required a bean of type ‘pl.piomin.services.employee.mapper.EmployeeMapper’ that could not be found.

Action:
Consider defining a bean of type ‘pl.piomin.services.employee.mapper.EmployeeMapper’ in your configuration.

I cloned your repository and it didn’t work either. Thanks

comments user
Onjee Kim

Why is no one commenting on such a great article. I saw your really good and intuitive writing well. I’ve been getting into event-driven architecture and modular monolith lately. Among them, I was thinking about how to eliminate the dependence related to the entity, and I saw this article, which was very helpful. Thank you from the bottom of my heart.

Let me ask you one more question. Should I remove constraints such as foreign key constraints at database level? At my level, yes. If my analogy is correct (even if it’s to go to msa or more future architecture), it seems that this article also considered the logic of synchronizing based on events without considering separate database constraints. What advice can I get?

    comments user
    piotr.minkowski

    Well, there are some comments and I have some feedback on Twitter or Reddit 🙂

    Yes, foreign keys are not something that in general matches the modulith idea.

comments user
Soheil Qalamkari

Thanks for sharing this article.
Let’s imagine we are in a microservices zone,so we must implement cross cutting concerns in gateway like security,….. or with spring cloud gateway writing filters and so on.So we have not spring cloud gateway to use that power.So should we implement this concepts from scratch here?I mean gateway is a more than for exposing apis,and this architecture good for a day we want to transform to microservices,so should we let this out until goal day?

    comments user
    piotr.minkowski

    Well, you can also include Spring Cloud Gateway to such app if you need to it. Nothing wrong with that

comments user
MarioKusek

Excelent blog. Thank you.

I would add jMolecules in the mix. At least I would add AggregateRoot and Identifier.

    comments user
    piotr.minkowski

    Thanks. I’m not not very familiar with jMolecules. Could you elaborate?

comments user
OCANSEY KATEY GAD

Great articile. I now fully understand the springboot3 modulith concept.

    comments user
    piotr.minkowski

    Thanks!

comments user
suneel pervaiz

Greate article But how can we do global exception handling in spring boot modulith

    comments user
    piotr.minkowski

    The same as for the standard Spring Boot app. Spring Modulith doesn’t bring anything additional in this area

Leave a Reply