Spring Modulith Tutorial

Spring Modulith is a project within the Spring ecosystem that provides support for modularizing Spring Boot applications. It helps in structuring applications into modules, making them easier to develop, maintain, and test. This tutorial will guide you through the process of building a modular Spring Boot application using Spring Modulith.

Prerequisites

  • JDK 17 or later
  • Maven or Gradle
  • Spring Boot (version 3.2+ recommended)
  • An IDE (IntelliJ IDEA, Eclipse, VS Code, etc.)

Overview

  1. Introduction to Spring Modulith
  2. Setting Up the Project
  3. Creating Modules
  4. Inter-module Communication
  5. Testing Modules
  6. Monitoring and Observing Modules

Step 1: Introduction to Spring Modulith

Spring Modulith aims to improve the modularization of Spring applications by providing a structured approach to dividing an application into distinct modules. Each module can encapsulate its own logic, dependencies, and configuration, promoting separation of concerns and improving maintainability.

Step 2: Setting Up the Project

2.1 Generate the Project

Use Spring Initializr to generate a new Spring Boot project with the following configuration:

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 3.2.x
  • Dependencies: Spring Web, Spring Data JPA, Lombok, H2 Database

2.2 Download and Open the Project

Download the generated project, unzip it, and open it in your IDE.

Example Project Structure

spring-modulith-app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/modulith/
│   │   │       └── ModulithApplication.java
│   │   │       └── customer/
│   │   │           └── CustomerModule.java
│   │   │           └── model/
│   │   │               └── Customer.java
│   │   │           └── service/
│   │   │               └── CustomerService.java
│   │   │           └── repository/
│   │   │               └── CustomerRepository.java
│   │   │       └── order/
│   │   │           └── OrderModule.java
│   │   │           └── model/
│   │   │               └── Order.java
│   │   │           └── service/
│   │   │               └── OrderService.java
│   │   │           └── repository/
│   │   │               └── OrderRepository.java
│   │   └── resources/
│   │       ├── application.properties
│   └── test/
│       └── java/
│           └── com/example/modulith/
│               └── ModulithApplicationTests.java
├── mvnw
├── mvnw.cmd
├── pom.xml
└── .mvn/
    └── wrapper/
        └── maven-wrapper.properties

Step 3: Creating Modules

3.1 Define Modules

Modules can be defined as packages within the src/main/java/com/example/modulith directory. Each module encapsulates its own components such as models, repositories, and services.

Creating the Customer Module

Create the Customer module in the src/main/java/com/example/modulith/customer directory.

package com.example.modulith.customer;

import org.springframework.modulith.Module;

@Module
public class CustomerModule {
}

Creating the Customer Model

Create the Customer entity in the src/main/java/com/example/modulith/customer/model directory.

package com.example.modulith.customer.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Customer {

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

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Creating the Customer Repository

Create a repository interface CustomerRepository in the src/main/java/com/example/modulith/customer/repository directory.

package com.example.modulith.customer.repository;

import com.example.modulith.customer.model.Customer;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

Creating the Customer Service

Create a service class CustomerService in the src/main/java/com/example/modulith/customer/service directory.

package com.example.modulith.customer.service;

import com.example.modulith.customer.model.Customer;
import com.example.modulith.customer.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    public List<Customer> getAllCustomers() {
        return customerRepository.findAll();
    }

    public Optional<Customer> getCustomerById(Long id) {
        return customerRepository.findById(id);
    }

    public Customer createCustomer(Customer customer) {
        return customerRepository.save(customer);
    }

    public Optional<Customer> updateCustomer(Long id, Customer customerDetails) {
        return customerRepository.findById(id).map(customer -> {
            customer.setName(customerDetails.getName());
            customer.setEmail(customerDetails.getEmail());
            return customerRepository.save(customer);
        });
    }

    public void deleteCustomer(Long id) {
        customerRepository.deleteById(id);
    }
}

3.2 Define Another Module (Order Module)

Creating the Order Module

Create the Order module in the src/main/java/com/example/modulith/order directory.

package com.example.modulith.order;

import org.springframework.modulith.Module;

@Module
public class OrderModule {
}

Creating the Order Model

Create the Order entity in the src/main/java/com/example/modulith/order/model directory.

package com.example.modulith.order.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

import com.example.modulith.customer.model.Customer;

@Entity
public class Order {

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

    @ManyToOne
    private Customer customer;

    private String product;
    private Integer quantity;

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    public String getProduct() {
        return product;
    }

    public void setProduct(String product) {
        this.product = product;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
}

Creating the Order Repository

Create a repository interface OrderRepository in the src/main/java/com/example/modulith/order/repository directory.

package com.example.modulith.order.repository;

import com.example.modulith.order.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}

Creating the Order Service

Create a service class OrderService in the src/main/java/com/example/modulith/order/service directory.

package com.example.modulith.order.service;

import com.example.modulith.order.model.Order;
import com.example.modulith.order.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }

    public Optional<Order> getOrderById(Long id) {
        return orderRepository.findById(id);
    }

    public Order createOrder(Order order) {
        return orderRepository.save(order);
    }

    public Optional<Order> updateOrder(Long id, Order orderDetails) {
        return orderRepository.findById(id).map(order -> {
            order.setProduct(orderDetails.getProduct());
            order.setQuantity(orderDetails.getQuantity());
            return orderRepository.save(order);
        });
    }

    public void deleteOrder(Long id) {
        orderRepository.deleteById(id);
    }
}

Step 4: Inter-module Communication

Modules can communicate with each other using Spring's event system or by calling services directly.

4.1 Event-Based Communication

Defining Events

Create an event class CustomerCreatedEvent in the src/main/java/com/example/modulith/customer/event directory.

package com.example.modulith.customer.event;

import com.example.modulith.customer.model.Customer;
import org.springframework.context.ApplicationEvent;

public class CustomerCreatedEvent extends ApplicationEvent {

    private final Customer customer;

    public CustomerCreatedEvent(Object source, Customer customer) {
        super(source);
        this.customer = customer;
    }

    public Customer getCustomer() {
        return customer;
    }
}

Publishing Events

Modify the CustomerService to publish an event when a new customer is created.

package com.example.modulith.customer.service;

import com.example.modulith.customer.event.CustomerCreatedEvent;
import com.example.modulith.customer.model.Customer;
import com.example.modulith.customer.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public List<Customer> getAllCustomers() {
        return customerRepository.findAll();
    }

    public Optional<Customer> getCustomerById(Long id) {
        return customerRepository.findById(id);
    }

    public Customer createCustomer(Customer customer) {
        Customer savedCustomer = customerRepository.save(customer);
        eventPublisher.publishEvent(new CustomerCreatedEvent(this, savedCustomer));
        return savedCustomer;
    }

    public Optional<Customer> updateCustomer(Long id, Customer customerDetails) {
        return customerRepository.findById(id).map(customer -> {
            customer.setName(customerDetails.getName());
            customer.setEmail(customerDetails.getEmail());
            return customerRepository.save(customer);
        });
    }

    public void deleteCustomer(Long id) {
        customerRepository.deleteById(id);
    }
}

Listening to Events

Create an event listener in the OrderService to handle CustomerCreatedEvent.

package com.example.modulith.order.service;

import com.example.modulith.customer.event.CustomerCreatedEvent;
import com.example.modulith.order.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }

    public Optional<Order> getOrderById(Long id) {
        return orderRepository.findById(id);
    }

    public Order createOrder(Order order) {
        return orderRepository.save(order);
    }

    public Optional<Order> updateOrder(Long id, Order orderDetails) {
        return orderRepository.findById(id).map(order -> {
            order.setProduct(orderDetails.getProduct());
            order.setQuantity(orderDetails.getQuantity());
            return orderRepository.save(order);
        });
    }

    public void deleteOrder(Long id) {
        orderRepository.deleteById(id);
    }

    @EventListener
    public void handleCustomerCreatedEvent(CustomerCreatedEvent event) {
        System.out.println("Customer created: " + event.getCustomer().getName());
        // Additional logic can be added here
    }
}

Step 5: Testing Modules

Create test classes for the modules in the src/test/java/com/example/modulith directory.

package com.example.modulith.customer;

import com.example.modulith.customer.model.Customer;
import com.example.modulith.customer.service.CustomerService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class CustomerModuleTests {

    @Autowired
    private CustomerService customerService;

    @Test
    void testCreateCustomer() {
        Customer customer = new Customer();
        customer.setName("John Doe");
        customer.setEmail("[email protected]");
        Customer savedCustomer = customerService.createCustomer(customer);
        assertThat(savedCustomer.getId()).isNotNull();
        assertThat(savedCustomer.getName()).isEqualTo("John Doe");
    }
}
package com.example.modulith.order;

import com.example.modulith.order.model.Order;
import com.example.modulith.order.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class OrderModuleTests {

    @Autowired
    private OrderService orderService;

    @Test
    void testCreateOrder() {
        Order order = new Order();
        order.setProduct("Laptop");
        order.setQuantity(1);
        Order savedOrder = orderService.createOrder(order);
        assertThat(savedOrder.getId()).isNotNull();
        assertThat(savedOrder.getProduct()).isEqualTo("Laptop");
    }
}

Step 6: Monitoring and Observing Modules

Using Spring Boot Actuator

Add the Actuator dependency to your pom.xml.

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

Enabling Actuator Endpoints

Configure Actuator endpoints in the src/main/resources/application.properties file.

management.endpoints.web.exposure.include=*

Accessing Actuator Endpoints

Run your application and access the Actuator endpoints:

  • Health: http://localhost:8080/actuator/health
  • Info: http://localhost:8080/actuator/info
  • Metrics: http://localhost:8080/actuator/metrics

Conclusion

In this tutorial, we covered how to build a modular Spring Boot application using Spring Modulith. We went through setting up the project, creating modules, inter-module communication, testing modules, and monitoring the application using Actuator. Following these steps, you can create well-structured and maintainable Spring Boot applications.

Comments