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