Blog
navigate_next
Software Engineering
Spring Modulith
Gaurav Sharma
December 5, 2024

Building a modular application using Spring Modulith

Spring Modulith  is a submodule inside the Spring framework that helps developers structure their monolithic Spring applications in a more organized and maintainable way. It addresses the common challenge of code becoming complex and intertwined as applications grow larger.

At its core, Spring Modulith introduces the concept of application modules—distinct parts of your application that handle specific business capabilities. These modules maintain clear boundaries and communicate through well-defined interfaces.

The Problem Spring Modulith Solves:

<span class="pink">The challenge before was that developers could easily break module boundaries and create unwanted dependencies, as there was no enforcement.The code would still work, but the modular structure would gradually break down.</span>

Previously, maintaining strict module boundaries in a Spring Boot application was challenging. Developers could inadvertently create unwanted dependencies, breaking the modular structure over time. While the code would still function, the architecture would lose its integrity.

Spring Modulith introduces a solution to enforce module boundaries effectively. By integrating this dependency, the system actively prevents such mistakes. If you attempt to access restricted code or establish improper dependencies between modules, your tests will fail. This proactive approach ensures your modular structure remains clean, consistent, and maintainable as your application evolves.

For instance, in an e-commerce system, you might have separate modules for order processing, inventory management, and customer accounts.

The framework works with standard Spring Boot applications and uses conventional package structures to define module boundaries.

To use Spring Modulith effectively, your project should be running on:
  • Spring Boot version 3 or higher
  • Java Development Kit (JDK) version 17 or higher

💡 In Spring Modulith, each top-level package within the root package is treated as a separate module. Any public class, interface, or other public element directly within these module packages is considered a "public API," making it accessible to other modules so that modules can communicate with each other using these public APIs.

However, if a public element is located within a subpackage of a module (nested package), Spring Modulith treats it as private to that module. This implies that even though these elements are public, they are by default hidden and inaccessible from other modules.

And even forcefully, if you try to use public APIs located in a module’s nested subpackages (which Spring Modulith considers private by default), Spring Modulith will fail certain tests. This indicates that your code isn’t truly modular or that something is incorrect in your design with an appropriate message.

What sets Spring Modulith apart is its enforcement capabilities. It actively monitors and validates module interactions during development and testing.This ensures that modules only communicate through their intended public interfaces, preventing unwanted dependencies and maintaining architectural integrity.

Spring Modulith includes built-in support for testing modules in isolation, generating documentation about module relationships, and analyzing dependencies between different parts of your application. It also works well with event-driven architectures, supporting communication between modules through events.

💡 Note: Spring Modulith works particularly well with SQL databases.

Always remember that modular monolith is an architecture, and spring modulith is a technology used to implement that architecture.

Synchronous Communication between Modules: Using Public API’s

Create a Spring Boot Maven project using start.spring.io with Java 21, and include the following dependencies:

  • Spring Modulith
  • Spring Web
  • Lombok
  • PostgreSQL Driver
  • Spring Data JPA

Synchronous Communication between Modules: Using Public API’s

pom.xml file should look something like this:

	
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.unlogged</groupId>
    <artifactId>GadgetGarage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>GadgetGarage</name>
    <description>GadgetGarage</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
        <spring-modulith.version>1.2.4</spring-modulith.version>
    </properties>
    <dependencies>

        <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>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.modulith</groupId>
            <artifactId>spring-modulith-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.modulith</groupId>
                <artifactId>spring-modulith-bom</artifactId>
                <version>${spring-modulith.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

every package inside <span class="pink">com.unlogged.gadgetgarage</span>(root package) is considered by default as a module by Spring Modulith.

  • Public APIs are in the root package of each module
  • Internal implementations are in <span class="pink">.internal</span> packages
  • Modules communicate only through public APIs and events.

💡 NotNote: To demonstrate how to organize a modular project structure, this project only includes the Order and Product modules. A full-fledged e-commerce application could introduce several additional modules, each thoughtfully designed around specific domains to enhance functionality and scalability.

Project modules structure

We have 2 modules: orders and product, and each module has an internal package and public APIs. The internal package is created for putting internal implementation classes such as domain, repository, and service classes that must be hidden from other modules. And the other classes, such as  DTO and service interface, are used by external modules for module communication and method calling.

Project modules structure example

Project modules structure example marking public APIs

In a modular monolith using Spring Modulith:

  1. Public APIs must be placed directly inside the module's root package to be accessible by other modules
  2. Any code in subpackages is considered private by default and cannot be accessed from outside the module
  3. Even within the same module, files in subpackages must be declared public to be accessed by other packages in that module

Project modules structure example showing a nested sub-package

Order.java

	
package com.unlogged.gadgetgarage.orders.internal;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    private String status;
}

<span class="pink">Order Entity</span>: Represents an order in the system, containing a list of order items and a status field to track the order state.

OrderItem.java

	
package com.unlogged.gadgetgarage.orders.internal;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private Integer quantity;
}

<span class="pink">OrderItem Entity</span>: represents dividual items in an order, including the product ID and quantity.

OrderRepository.java

	
package com.unlogged.gadgetgarage.orders.internal;

import org.springframework.data.jpa.repository.JpaRepository;

interface OrderRepository extends JpaRepository<Order, Long> {
}

<span class="pink">OrderRepository</span>: A JPA repository interface for managing <span class="pink">Order</span> entities, providing CRUD operations.

OrderServiceImpl.java

	
package com.unlogged.gadgetgarage.orders.internal;

import com.unlogged.gadgetgarage.orders.OrderDto;
import com.unlogged.gadgetgarage.orders.OrderItemDto;
import com.unlogged.gadgetgarage.orders.OrderService;
import com.unlogged.gadgetgarage.product.ProductService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final ProductService productService;

    OrderServiceImpl(OrderRepository orderRepository, ProductService productService) {
        this.orderRepository = orderRepository;
        this.productService = productService;
    }

    @Override
    public OrderDto createOrder(List<OrderItemDto> items) {
        // Verify all products exist
        items.forEach(item -> productService.findProduct(item.productId()));

        Order order = new Order();
        order.setItems(items.stream()
                .map(this::toOrderItem)
                .collect(Collectors.toList()));
        order.setStatus("CREATED");

        order = orderRepository.save(order);
        return toDto(order);
    }

    @Override
    public OrderDto findOrder(Long id) {
        return orderRepository.findById(id)
                .map(this::toDto)
                .orElseThrow(() -> new RuntimeException("Order not found"));
    }

    @Override
    public List<OrderDto> findAllOrders() {
        return orderRepository.findAll().stream()
                .map(this::toDto)
                .collect(Collectors.toList());
    }

    private OrderDto toDto(Order order) {
        return new OrderDto(
                order.getId(),
                order.getItems().stream()
                        .map(item -> new OrderItemDto(item.getProductId(), item.getQuantity()))
                        .collect(Collectors.toList()),
                order.getStatus()
        );
    }

    private OrderItem toOrderItem(OrderItemDto dto) {
        OrderItem item = new OrderItem();
        item.setProductId(dto.productId());
        item.setQuantity(dto.quantity());
        return item;
    }
}

<span class="pink">OrderServiceImpl</span>: Implements the business logic for order creation, retrieval, and mapping between entities and DTOs. It validates products via the <span class="pink">ProductService</span>.

OrderDto.java

	
package com.unlogged.gadgetgarage.orders;


import java.util.List;

public record OrderDto(
        Long id,
        List<OrderItemDto> items,
        String status
) {}

<span class="pink">OrderDto</span>: A lightweight data transfer object to expose order details, including items and status.

OrderItemDto.java

	
package com.unlogged.gadgetgarage.orders;


public record OrderItemDto(Long productId, Integer quantity) {}

<span class="pink">OrderItemDto</span>: A DTO to represent individual order item details like product ID and quantity.

OrderService.java

	
package com.unlogged.gadgetgarage.orders;

import java.util.List;

public interface OrderService {
    OrderDto createOrder(List<OrderItemDto> items);

    OrderDto findOrder(Long id);

    List<OrderDto> findAllOrders();
}

<span class="pink">OrderService</span> Interface: Defines the contract for order-related operations like creating and fetching orders.

Product.java

	
package com.unlogged.gadgetgarage.product.internal;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class Product {

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

}

<span class="pink">Product</span> Entity: Represents a product including fields for name, description, and price.

ProductRepository.java

	
package com.unlogged.gadgetgarage.product.internal;

import org.springframework.data.jpa.repository.JpaRepository;

interface ProductRepository extends JpaRepository<Product, Long> {
}

<span class="pink">ProductRepository</span>: A JPA repository interface for managing <span class="pink">Product</span> entities with basic CRUD capabilities.

ProductServiceImpl.java

	
package com.unlogged.gadgetgarage.product.internal;

import com.unlogged.gadgetgarage.product.ProductDto;
import com.unlogged.gadgetgarage.product.ProductService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
class ProductServiceImpl implements ProductService {
    private final ProductRepository productRepository;

    ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public ProductDto findProduct(Long id) {
        return productRepository.findById(id)
                .map(this::toDto)
                .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @Override
    public ProductDto createProduct(ProductDto dto) {
        Product product = toEntity(dto);
        product = productRepository.save(product);
        return toDto(product);
    }

    @Override
    public void updateProduct(Long id, ProductDto dto) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found"));
        updateEntity(product, dto);
        productRepository.save(product);
    }

    @Override
    public List<ProductDto> findAllProducts() {
        return productRepository.findAll().stream()
                .map(this::toDto)
                .collect(Collectors.toList());
    }

    private ProductDto toDto(Product product) {
        return new ProductDto(
                product.getId(),
                product.getName(),
                product.getDescription(),
                product.getPrice()
        );
    }

    private Product toEntity(ProductDto dto) {
        Product product = new Product();
        updateEntity(product, dto);
        return product;
    }

    private void updateEntity(Product product, ProductDto dto) {
        product.setName(dto.name());
        product.setDescription(dto.description());
        product.setPrice(dto.price());
    }
}

<span class="pink">ProductServiceImpl</span>: Implements the product-related business logic, including creating, updating, and retrieving products.

ProductDto.java

	
package com.unlogged.gadgetgarage.product;

public record ProductDto(
        Long id,
        String name,
        String description,
        Double price
) {
}

<span class="pink">ProductDto</span>: A DTO to transfer product details between layers in a structured manner.

ProductService.java

	
package com.unlogged.gadgetgarage.product;

import java.util.List;

public interface ProductService {
    ProductDto findProduct(Long id);
    ProductDto createProduct(ProductDto product);
    void updateProduct(Long id, ProductDto product);
    List<ProductDto> findAllProducts();
}

<span class="pink">ProductService Interface</span>

This interface defines the contract for handling product-related operations within the <span class="pink">product</span> module

GadgetGarageApplication.java

	
package com.unlogged.gadgetgarage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.modulith.Modulith;

@SpringBootApplication
@Modulith
public class GadgetGarageApplication {

    public static void main(String[] args) {
        SpringApplication.run(GadgetGarageApplication.class, args);
    }

}

<span class="pink">GadgetGarageApplication</span>: The main entry point of the Spring Boot application, annotated with <span class="pink">@Modulith</span> to signify the use of Spring Modulith for modular architecture.

Now, we have created modules and written the code, but how do we verify that the application is actually modular in nature?

Here comes the arch unit to save the day, which is inbuilt in spring modulith.

<span class="pink">Spring Modulith builds upon the robust foundation of the ArchUnit library to enforce architectural boundaries and design principles.</span>

Enforcing Architectural Boundaries in Spring Modular Monolith using ArchUnit

ArchUnit typo logo mark

In the world of enterprise applications, maintaining architectural integrity becomes increasingly challenging as systems grow. While microservices offer clear physical boundaries, many organizations opt for modular monoliths as a pragmatic middle ground.

However, without proper enforcement, these modular boundaries can easily blur over time.

This is where ArchUnit comes to the rescue.

ArchUnit:

ArchUnit is a powerful library that tests architecture and coding rules using unit tests.

Key functionalities:

  • Automated verification of architectural constraints
  • Integration with existing testing frameworks
  • Clear, readable rule definitions
  • Fast feedback during development
  • Documentation of architectural decisions through code

Writing a test to check if the modules of the entire application are truly modular in nature or if any cyclic dependencies exist between them:

	
   @Test
    void verifyModularity() {
        ApplicationModules.of(GadgetGarageApplication.class).verify();
    }

Output after running this test to check modularity between modules:

Console output after running this test to check modularity between modules:

Tests passed mean that modules are moular in nature; no boundaries are crossed between modules.

Generating Documentation of the whole application and individual modules from Spring Modulith Using Unit Tests

Spring Modulith also provides a convenient way to generate architectural documentation through unit tests, automatically creating diagrams that visualize your application's modular structure. These tests examine your codebase and produce PlantUML diagrams showing both the overall module relationships and detailed views of individual modules, making it easier to understand and maintain your application's architecture.

We are creating a diagram of our Spring Boot project that includes the order and product modules.

First write this test in your main test class.

	
    @Test
    void generateDocumentation() {
        new Documenter(ApplicationModules.of(GadgetGarageApplication.class))
                .writeModulesAsPlantUml()
                .writeIndividualModulesAsPlantUml();
    }

Explanation: This test code generates visual documentation for the GadgetGarage application.

When executed, it creates PlantUML diagrams that visualize the application's modular structure in two ways: first, it produces a comprehensive diagram showing all modules and their relationships with each other through the <span class="pink">writeModulesAsPlantUml()</span> method, and second, it creates individual, detailed diagrams for each module using <span class="pink">writeIndividualModulesAsPlantUml()</span>.

a folder named  <span class="pink">spring-modulith-docs</span> get generated inside <span class="pink">target</span> folder within your root application folder, which contains puml files

Snapshot of opening the components.puml files with the help of a puml plugin in IntelliJ

opening the <span class="pink">components.puml</span> files with the help of a puml plugin in IntelliJ

Diagram of an application structure consisting of Orders module and and Product module

Managing Module Boundaries with <span class="pink">package-info.java</span> in Spring Modulith

<span class="pink">package-info.java</span> serves as the access control definition for Spring Modulith modules. It defines clear boundaries of what code can be accessed between different parts of your application.

Spring Modulith verifies these boundaries through tests:

	
    @Test
    void verifyModularity() {
        ApplicationModules.of(GadgetGarageApplication.class).verify();
    }

This verification ensures no module oversteps its boundaries. It catches architectural violations early, preventing the gradual decay that often plagues large applications.<span class="pink">package-info.java</span> transforms module organization from a suggestion into an enforced reality.

For example: When a new developer joins the team, <span class="pink">package-info.java</span> serves as living documentation. It answers critical questions: What does this module do? What can it depend on? How should other modules interact with it?

These answers come not from outdated documentation but from the actual structure of the code.

The real value emerges in large applications. As systems grow, the clarity enforced by package-info.java prevents the chaos that often comes with scale. Modules remain focused and independent, making the system easier to understand, maintain, and evolve.

Suppose we have an application with several modules like <span class="pink">orders, product, payment, inventory</span>, and <span class="pink">shared</span>. If we want the <span class="pink">orders</span> module to communicate only with specific modules, we can explicitly define these dependencies in a <span class="pink">package-info.java</span> file within the <span class="pink">orders</span> module. To do this, create a <span class="pink">package-info.java</span> file directly inside the root of the <span class="pink">orders</span> module, and the code would look like this:

	
@org.springframework.modulith.ApplicationModule(
        allowedDependencies = {"product"}
)
package com.unlogged.gadgetgarage.orders;

The <span class="pink">@ApplicationModule</span> annotation marks the <span class="pink">orders</span> package as a module and allows it to depend only on the <span class="pink">product</span> module. If no modules are specified in <span class="pink">allowedDependencies</span>, all public APIs from other modules in the application are accessible to the <span class="pink">orders</span> module by default.

In Spring Modulith, package visibility follows a specific pattern for modular monoliths.

1. Root Package Visibility:When interfaces or classes are directly in a module's root package, they are automatically visible to other modules.

	
// This interface in root package is visible to all modules
package com.unlogged.gadgetgarage.orders;  // root package
public interface OrderService { }

2. Subpackage Privacy Rule:When you create any subpackage (like 'api', 'internal', 'service'), everything inside becomes private to that module by default.

	
// This class becomes private to orders module
package com.unlogged.gadgetgarage.orders.internal;  // subpackage
public class OrderServiceImpl { ...}  // other modules can't see this

💡 The <span class="pink">'internal'</span> package in Spring Modulith has special significance. It is reserved for truly private implementations.

3. Making Subpackages Public:

By default, Spring Modulith keeps everything in subpackages private for better encapsulation.

<span class="pink">But when you need to share certain interfaces or classes with other modules, you can use @NamedInterface to make specific subpackages accessible, while still maintaining clear boundaries and preventing messy dependencies.</span>

To make a subpackage accessible to other modules, you need to focus on certain things:

  • A package-info.java file in that subpackage
  • The @NamedInterface annotation in package-info.java
  • No circular dependencies allowed between modules (example: orders module can't use product module and vice-versa at the same time)
  • Only public classes/interfaces in the named package will be visible

Example: If we modify our earlier project folder structure and move the <span class="pink">OrderService</span>,  <span class="pink">ProductService</span> and other public APIs into sub-package named <span class="pink">api</span> within the <span class="pink">orders</span> and product modules, respectively, by Spring Modulith's definition, all classes, interfaces, and other components inside the <span class="pink">api</span> sub-package will be treated as private since it resides within the module's package.

The restructured folder should look like this:

Snapshot of the restructured application folder

When running the modularity test, the following error is displayed in the console, indicating a failure:

<span class="pink">org.springframework.modulith.core.Violations: - Module 'orders' depends on non-exposed type com.unlogged.gadgetgarage.product.api.ProductService within module 'product'!</span>

Modularity test run error indicating dependence on a non-exposed type within the "product module".

To resolve this issue, we create a file named <span class="pink">package-info.java</span> and include the below code. Since the orders module depends on the product module, we explicitly expose all the APIs within the <span class="pink">api</span> package of the product module.

	
@org.springframework.modulith.NamedInterface("ProductsAPI")
package com.unlogged.gadgetgarage.product.api;

Explanation: This annotation (<span class="pink">@NamedInterface("ProductsAPI"</span>)) marks the product.api package as a public part of the product module, meaning other modules can access its contents. It helps clearly define what is meant to be shared while keeping other parts of the module private.

This will fix the issue, and all the modularity tests will pass.

Snapshot of modularity passing status

Avoiding Cyclic Dependencies

A cyclic dependency occurs when one module, say Module A, depends on another module, Module B, and in turn, Module B depends back on Module A.

In Spring modulith, this creates build failures with tools like Maven or Gradle because they can't determine which module to compile first.

The Spring framework also fails during runtime startup; it can't initialize these modules since each needs the other to exist first.

To fix this:

  1. Extract shared code to a new base module
  2. Use interfaces to break direct dependencies
  3. Restructure modules to remove the circular relationship
Cyclic dependencies indicate modules that are too tightly coupled—they tend to change together and are difficult to maintain separately.

Cyclic dependencies indicate modules that are too tightly coupled—they tend to change together and are difficult to maintain separately.

For a quick example:

Adding just two lines of code in the <span class="pink">ProductServiceImpl</span> class that even do not have any implementation creates a cyclic depedency between orders and the product module and fails the test.

	
    @EventListener
    public void handleOrderEvent(OrderCreatedEvent event) {

    }

Snapshot showing the highlighted part in grey informing  that the orders module depends on the product module, and the product module depends on the orders module, which in turn creates a cyclic dependency and fails the modularity test.

The highlighted part in grey informs us that the orders module depends on the product module, and the product module depends on the orders module, which in turn creates a cyclic dependency and fails the modularity test.

The cycle is:

💡 orders -> product -> orders

  • <span class="pink">orders</span> module depends on  <span class="pink">product</span> module (via ProductService in  <span class="pink">OrderServiceImpl</span>)
	
// In OrderServiceImpl.java
class OrderServiceImpl implements OrderService {
    private final ProductService productService; // Dependency on product module

    @Override
    public OrderDto createOrder(List<OrderItemDto> items) {
        items.forEach(item -> productService.findProduct(item.productId())); // Using product module
        // ... 
    }
}

  • product module depends on orders module (via OrderCreatedEvent in ProductServiceImpl)
  • We have created an OrderCreatedEvent in the event package, which is a subpackage of the orders module.
	
// In ProductServiceImpl.java
class ProductServiceImpl implements ProductService {
    @EventListener
    public void handleOrderEvent(OrderCreatedEvent event) { // Dependency on orders module via event
        
    }
}

and thus cyclic dependency exists between the modules and build fails for Maven, Gradle, etc. So try to put the common code in a shared module and make it public or use async communication like events.

Conclusion:

Modular monoliths and Spring Modulith are extensive topics that require weeks or even months of research and study to fully grasp. If this topic interests you and you're looking to get hands-on experience or transition an existing project from a monolith—or even return from microservices—feel free to dive in.

But this article provides more than enough to help you get started and operational. In upcoming articles, we’ll explore module communication using asynchronous events for a more loosely coupled system, discuss the concept of private tables for each module in the database, and delve into detailed testing strategies to ensure the robustness of your modular architecture.

References:

Gaurav Sharma
November 25, 2024
Use Unlogged to
mock instantly
record and replay methods
mock instantly
Install Plugin