Prerequisites
- JDK 17 or later
- Maven or Gradle
- Spring Boot (version 3.2+ recommended)
- An IDE (IntelliJ IDEA, Eclipse, VS Code, etc.)
Overview
- Introduction to Spring Modulith
- Setting Up the Project
- Creating Modules
- Inter-module Communication
- Testing Modules
- 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("john.doe@example.com");
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
Post a Comment
Leave Comment