Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Sunshine And March Vibes (2025 Wallpapers Edition)

      May 16, 2025

      The Case For Minimal WordPress Setups: A Contrarian View On Theme Frameworks

      May 16, 2025

      How To Fix Largest Contentful Paint Issues With Subpart Analysis

      May 16, 2025

      How To Prevent WordPress SQL Injection Attacks

      May 16, 2025

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025

      Minecraft licensing robbed us of this controversial NFL schedule release video

      May 16, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      The power of generators

      May 16, 2025
      Recent

      The power of generators

      May 16, 2025

      Simplify Factory Associations with Laravel’s UseFactory Attribute

      May 16, 2025

      This Week in Laravel: React Native, PhpStorm Junie, and more

      May 16, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025
      Recent

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Build Multi-Module Projects in Spring Boot for Scalable Microservices

    How to Build Multi-Module Projects in Spring Boot for Scalable Microservices

    November 12, 2024

    As software applications grow in complexity, managing scalability, modularity, and clarity becomes essential.

    Spring Boot’s multi-module structure allows you to manage different parts of the application independently, which lets your team develop, test, and deploy components separately. This structure keeps code organized and modular, making it useful for both microservices and large monolithic systems.

    In this tutorial, you’ll build a multi-module Spring Boot project, with each module dedicated to a specific responsibility. You’ll learn how to set up modules, configure inter-module communication, handle errors, implement JWT-based security, and deploy using Docker.

    Prerequisites:

    • Basic knowledge of Spring Boot and Maven.

    • Familiarity with Docker and CI/CD concepts (optional but helpful).

    Table of Contents

    1. Why Multi-Module Projects?

    2. Project Structure and Architecture

    3. How to Set Up the Parent Project

    4. How to Create the Modules

    5. Inter-Module Communication

    6. Common Pitfalls and Solutions

    7. Testing Strategy

    8. Error Handling and Logging

    9. Security and JWT Integration

    10. Deployment with Docker and CI/CD

    11. Best Practices and Advanced Use Cases

    12. Conclusion and Key Takeaways

    1. Why Multi-Module Projects?

    In single-module projects, components are often tightly coupled, making it difficult to scale and manage complex codebases. A multi-module structure offers several advantages:

    • Modularity: Each module is dedicated to a specific task, such as User Management or Inventory, simplifying management and troubleshooting.

    • Team Scalability: Teams can work independently on different modules, minimizing conflicts and enhancing productivity.

    • Flexible Deployment: Modules can be deployed or updated independently, which is particularly beneficial for microservices or large applications with numerous features.

    Real-World Example

    Consider a large e-commerce application. Its architecture can be divided into distinct modules:

    • Customer Management: Responsible for handling customer profiles, preferences, and authentication.

    • Product Management: Focuses on managing product details, stock, and pricing.

    • Order Processing: Manages orders, payments, and order tracking.

    • Inventory Management: Oversees stock levels and supplier orders.

    Case Study: Netflix

    To illustrate these benefits, let’s examine how Netflix employs a multi-module architecture.

    Netflix is a leading example of a company that effectively uses this approach through its microservices architecture. Each microservice at Netflix is dedicated to a specific function, such as user authentication, content recommendations, or streaming services.

    This modular structure enables Netflix to scale its operations efficiently, deploy updates independently, and maintain high availability and performance. By decoupling services, Netflix can manage millions of users and deliver content seamlessly worldwide, ensuring a robust and flexible system that supports its vast and dynamic platform.

    This architecture not only enhances scalability but also improves fault isolation, allowing Netflix to innovate rapidly and respond effectively to user demands.

    2. Project Structure and Architecture

    Now let’s get back to our example project. Your multi-module Spring Boot project will use five key modules. Here’s the layout:

    codespring-boot-multi-module/
     ├── common/               # Shared utilities and constants
     ├── domain/               # Domain entities
     ├── repository/           # Data access layer (DAL)
     ├── service/              # Business logic
     └── web/                  # Main Spring Boot application and controllers
    

    Each module has a specific role:

    • common: Stores shared utilities, constants, and configuration files used across other modules.

    • domain: Contains data models for your application.

    • repository: Manages database operations.

    • service: Encapsulates business logic.

    • web: Defines REST API endpoints and serves as the application’s entry point.

    This structure aligns with separation of concerns principles, where each layer is independent and handles its own logic.

    The diagram below illustrates the various modules:

    Diagram showing a software architecture with five modules: Web, Service, Repository, Domain, and Common, connected by arrows indicating relationships.

    3. How to Set Up the Parent Project

    Step 1: Create the Root Project

    Let’s run these commands to create the Maven parent project:

    mvn archetype:generate -DgroupId=com.example -DartifactId=spring-boot-multi-module -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
    cd spring-boot-multi-module
    

    Step 2: Configure the Parent pom.xml

    In the pom.xml, let’s define our dependencies and modules:

    <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 http://www.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.example</groupId>
        <artifactId>spring-boot-multi-module</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>pom</packaging>
        <modules>
            <module>common</module>
            <module>domain</module>
            <module>repository</module>
            <module>service</module>
            <module>web</module>
        </modules>
        <properties>
            <java.version>11</java.version>
            <spring.boot.version>2.5.4</spring.boot.version>
        </properties>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>${spring.boot.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>
                </plugin>
            </plugins>
        </build>
    </project>
    

    This pom.xml file centralizes dependencies and configurations, making it easier to manage shared settings across modules.

    4. How to Create the Modules

    Common Module

    Let’s create a common module to define shared utilities like date formatters. Create this module and add a sample utility class:

    mvn archetype:generate -DgroupId=com.example.common -DartifactId=common -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
    

    Date Formatter Utility:

    package com.example.common;
    
    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    
    public class DateUtils {
        public static String formatDate(LocalDate date) {
            return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        }
    }
    

    Domain Module

    In the domain module, you will define your data models.

    package com.example.domain;
    
    import javax.persistence.Entity;
    import javax.persistence.Id;
    
    @Entity
    public class User {
        @Id
        private Long id;
        private String name;
    
        // Getters and Setters
    }
    

    Repository Module

    Let’s create the repository module to manage data access. Here’s a basic repository interface:

    package com.example.repository;
    
    import com.example.domain.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface UserRepository extends JpaRepository<User, Long> {}
    

    Service Module

    Let’s create the service module to hold your business logic. Here’s an example service class:

    package com.example.service;
    
    import com.example.domain.User;
    import com.example.repository.UserRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserService {
    
        @Autowired
        private UserRepository userRepository;
    
        public User getUserById(Long id) {
            return userRepository.findById(id).orElse(null);
        }
    }
    

    Web Module

    The web module serves as the REST API layer.

    @RestController
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping("/users/{id}")
        public User getUserById(@PathVariable Long id) {
            return userService.getUserById(id);
        }
    }
    

    5. Inter-Module Communication

    To avoid direct dependencies, you can use REST APIs or message brokers (like Kafka) for inter-module communication. This ensures loose coupling and allows each module to communicate independently.

    The diagram below demonstrates how modules communicate with each other:

    Flowchart showing the interaction between modules in the software architecture: Web Module handles API endpoints and returns responses, Service Module executes business logic, Repository Module accesses data and returns processed data, and connects to a Database.

    The diagram illustrates how different system components communicate to handle requests efficiently.

    The Web Module processes incoming API requests and forwards them to the Service Module, which contains the business logic. The Service Module then interacts with the Repository Module to fetch or update data in the Database. This layered approach ensures that each module operates independently, promoting flexibility and easier maintenance.

    Example Using Feign Client:

    In the context of inter-module communication, using tools like Feign Clients is a powerful way to achieve loose coupling between services.

    The Feign client allows one module to seamlessly communicate with another through REST API calls, without requiring direct dependencies. This approach fits perfectly within the layered architecture described earlier, where the Service Module can fetch data from other services or microservices using Feign clients, rather than directly accessing databases or hard-coding HTTP requests.

    This not only simplifies the code but also improves scalability and maintainability by isolating service dependencies.

    @FeignClient(name = "userServiceClient", url = "http://localhost:8081")
    public interface UserServiceClient {
        @GetMapping("/users/{id}")
        User getUserById(@PathVariable("id") Long id);
    }
    

    6. Common Pitfalls and Solutions

    When implementing a multi-module architecture, you may encounter several challenges. Here are some common pitfalls and their solutions:

    • Circular Dependencies: Modules may inadvertently depend on each other, creating a circular dependency that complicates builds and deployments.

      • Solution: Carefully design module interfaces and use dependency management tools to detect and resolve circular dependencies early in the development process.
    • Over-Engineering: There’s a risk of creating too many modules, leading to unnecessary complexity.

      • Solution: Start with a minimal set of modules and only split further when there’s a clear need, ensuring each module has a distinct responsibility.
    • Inconsistent Configurations: Managing configurations across multiple modules can lead to inconsistencies.

      • Solution: Use centralized configuration management tools, such as Spring Cloud Config, to maintain consistency across modules.
    • Communication Overhead: Inter-module communication can introduce latency and complexity.

      • Solution: Optimize communication by using efficient protocols and consider asynchronous messaging where appropriate to reduce latency.
    • Testing Complexity: Testing a multi-module project can be more complex due to the interactions between modules.

      • Solution: Implement a robust testing strategy that includes unit tests for individual modules and integration tests for inter-module interactions.

    By being aware of these pitfalls and applying these solutions, you can effectively manage the complexities of a multi-module architecture and ensure a smooth development process.

    7. Testing Strategy and Configuration

    Testing each module independently and as a unit is critical in multi-module setups.

    Unit Tests

    Here, we’ll use JUnit and Mockito for performing unit tests:

    @RunWith(MockitoJUnitRunner.class)
    public class UserServiceTest {
    
        @Mock
        private UserRepository userRepository;
    
        @InjectMocks
        private UserService userService;
    
        @Test
        public void testGetUserById() {
            User user = new User();
            user.setId(1L);
            user.setName("John");
    
            Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(user));
    
            User result = userService.getUserById(1L);
            assertEquals("John", result.getName());
        }
    }
    

    Integration Tests

    And we’ll use Testcontainers with an in-memory database for integration tests:

    @Testcontainers
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    public class UserServiceIntegrationTest {
    
        @Container
        private static PostgreSQLContainer<?> postgresqlContainer = new PostgreSQLContainer<>("postgres:latest");
    
        @Autowired
        private UserService userService;
    
        @Test
        public void testFindById() {
            User user = userService.getUserById(1L);
            assertNotNull(user);
        }
    }
    

    8. Error Handling and Logging

    Error handling and logging ensure a robust and debuggable application.

    Error Handling

    In this section, we’ll explore how to handle errors gracefully in your Spring Boot application using a global exception handler. By using @ControllerAdvice, we’ll set up a centralized way to catch and respond to errors, keeping our code clean and our responses consistent.

    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(UserNotFoundException.class)
        public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
            return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND);
        }
    }
    

    In the code example above, we define a GlobalExceptionHandler that catches any UserNotFoundException and returns a friendly message like “User not found” with a status of 404. This way, you don’t have to handle this exception in every controller—you’ve got it covered in one place!

    Flowchart depicting a sequence of interactions between a Client, Web Module, Global Error Handler, and Logger. The process involves handling requests, processing them, and managing exceptions. If an exception occurs, it is handled and logged, followed by an error response. If no exception occurs, a successful response is returned.

    Now, let’s take a look at the diagram. Here’s how it all flows: when a client sends a request to our Web Module, if everything goes smoothly, you’ll get a successful response. But if something goes wrong, like a user not being found, the error will be caught by our Global Error Handler. This handler logs the issue and returns a clean, structured response to the client.

    This approach ensures that users get clear error messages while keeping your app’s internals hidden and secure.

    Logging

    Structured logging in each module improves traceability and debugging. You can use a centralized logging system like Logback and include correlation IDs to trace requests.

    9. Security and JWT Integration

    In this section, we’re going to set up JSON Web Tokens (JWT) to secure our endpoints and control access based on user roles. We’ll configure this in the SecurityConfig class, which will help us enforce who can access what parts of our application.

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt();
        }
    }
    

    In the code example above, you can see how we’ve defined access rules:

    • The /admin/** endpoints are restricted to users with the ADMIN role.

    • The /user/** endpoints can be accessed by users with either the USER or ADMIN roles.

    • Any other requests will require the user to be authenticated.

    Next, we set up our application to validate incoming tokens using .oauth2ResourceServer().jwt();. This ensures that only requests with a valid token can access our secured endpoints.

    Diagram depicting a sequence of interactions among five components: Client, Web Module, Security Filter, Service, and Repository. Arrows represent steps for token authentication and accessing a service, including requests, validations, data fetching, and responses, with outcomes for both valid and invalid tokens.

    Now, let’s walk through the diagram. When a client sends a request to access a resource, the Security Filter first checks if the provided JWT token is valid. If the token is valid, the request proceeds to the Service Module to fetch or process the data. If not, access is denied right away, and the client receives an error response.

    This flow ensures that only authenticated users can access sensitive resources, keeping our application secure.

    10. Deployment with Docker and CI/CD

    In this section, we’ll containerize each module using Docker to make our application easier to deploy and run consistently across different environments. We’ll also set up a CI/CD pipeline using GitHub Actions (but you can use Jenkins too if you prefer). Automating this process ensures that any changes you push are automatically built, tested, and deployed.

    Step 1: Containerizing with Docker

    We start by creating a Dockerfile for the Web Module:

    FROM openjdk:11-jre-slim
    COPY target/web-1.0-SNAPSHOT.jar app.jar
    ENTRYPOINT ["java", "-jar", "/app.jar"]
    

    Here, we’re using a lightweight version of Java 11 to keep our image size small. We copy the compiled .jar file into the container and set it up to run when the container starts.

    Step 2: Using Docker Compose for Multi-Module Deployment

    Now, we’ll use a Docker Compose file to orchestrate multiple modules together:

    version: '3'
    services:
      web:
        build: ./web
        ports:
          - "8080:8080"
      service:
        build: ./service
        ports:
          - "8081:8081"
    

    With this setup, we can run both the Web Module and the Service Module at the same time, making it easy to spin up the entire application with a single command. Each service is built separately from its own directory, and we expose the necessary ports to access them.

    CI/CD Example with GitHub Actions

    name: CI Pipeline
    
    on: [push, pull_request]
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v2
        - name: Set up JDK 11
          uses: actions/setup-java@v2
          with:
            java-version: '11'
        - name: Build with Maven
          run: mvn clean install
    

    This pipeline automatically kicks in whenever you push new code or create a pull request. It checks out your code, sets up Java, and runs a Maven build to ensure everything is working correctly.

    11. Best Practices and Advanced Use Cases

    The following best practices ensure maintainability and scalability.

    Best Practices

    • Avoid Circular Dependencies: Ensure modules don’t have circular references to avoid build issues.

    • Separate Concerns Clearly: Each module should focus on one responsibility.

    • Centralized Configurations: Manage configurations centrally for consistent setups.

    Advanced Use Cases

    1. Asynchronous Messaging with Kafka: Use Kafka for decoupled communication between services. Modules can publish and subscribe to events asynchronously.

    2. REST Client with Feign: Use Feign to call services within modules. Define a Feign client interface for communication.

    3. Caching for Performance: Use Spring Cache in the service module for optimizing data retrieval.

    Conclusion and Key Takeaways

    A multi-module Spring Boot project provides modularity, scalability, and ease of maintenance.

    In this tutorial, you learned to set up modules, manage inter-module communication, handle errors, add security, and deploy with Docker.

    Following best practices and using advanced techniques like messaging and caching will further optimize your multi-module architecture for production use.

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Hostinger
    Facebook Twitter Reddit Email Copy Link
    Previous ArticleHow to Build a Dropbox-like Distributed File Storage System Using MinIO and gRPC
    Next Article How to Simplify Your Git Commands with Git Aliases

    Related Posts

    Security

    Nmap 7.96 Launches with Lightning-Fast DNS and 612 Scripts

    May 17, 2025
    Common Vulnerabilities and Exposures (CVEs)

    CVE-2024-47893 – VMware GPU Firmware Memory Disclosure

    May 17, 2025
    Leave A Reply Cancel Reply

    Hostinger

    Continue Reading

    Understanding the faulty proteins linked to cancer and autism

    Artificial Intelligence

    Sniper Elite Resistance Xbox and PC review: Rebellion’s stealthy shooter is like comfort food for fans of the Nazi-blasting genre of gaming

    News & Updates

    Usability and Experience (UX) in Universal Design Series: Practical Applications – 3

    Development

    “We did not hit the mark”: TeamRICOCHET addresses shortcomings in Warzone and Black Ops 6’s anti-cheat integration ahead of changes for Season 2

    Development

    Highlights

    Turn your Meta Quest into a massive display for any HDMI device – here’s how

    August 16, 2024

    Now, you can connect your game console, laptop, or phone to any Quest headset. We…

    Microsoft is turning Windows Copilot into a regular app – and here’s why you’ll like it

    May 23, 2024

    South Korea Confronts Major Data Breach from Military Intelligence Command

    July 29, 2024

    The Best Email Parser in 2024

    May 27, 2024
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.