Spring Boot Tutorial
Multi-Module Spring Boot Projects
As an application grows, one giant module becomes hard to navigate and slow to build, and it is too easy for the web layer to reach straight into database code. Splitting a project into modules — for example domain, persistence, web and app — enforces boundaries at compile time, lets teams own separate parts, and allows shared libraries between several applications.
This lesson builds a multi-module Maven project (with the Gradle equivalent): a parent POM, library modules, a runnable Spring Boot module, component scanning across modules, and tips for testing and building.
When to Split into Modules
Modules are worthwhile when you need to share code between several deployable applications (an API and a batch worker using the same domain), enforce architectural layering, or speed up builds of a large code base. For a small service, a single module organised by packages (or checked with Spring Modulith) is usually simpler.
Structure
A common layout has a parent POM with <packaging>pom</packaging> listing the modules, library modules packaged as ordinary jars, and one (or more) application modules that contain the @SpringBootApplication class and the spring-boot-maven-plugin. Dependencies flow in one direction: app → web → service → domain; lower modules never depend on higher ones.
Only the Application Module Is Repackaged
The Spring Boot Maven plugin turns a jar into an executable "fat jar". Apply it only to application modules. If a library module were repackaged, other modules could no longer use its classes, because the fat-jar layout nests classes under BOOT-INF/classes.
Component Scanning Across Modules
@SpringBootApplication scans its own package and sub-packages. If all modules share a root package (com.webnest.shop.*) and the application class sits at com.webnest.shop, everything is found automatically. Otherwise specify scanBasePackages, @EnableJpaRepositories and @EntityScan explicitly.
Examples
Project layout and parent POM
webnest-shop/
├── pom.xml (parent, packaging pom)
├── shop-domain/ (entities, domain services — plain jar)
├── shop-persistence/ (Spring Data repositories — plain jar)
├── shop-web/ (controllers, DTOs — plain jar)
└── shop-app/ (@SpringBootApplication, application.yml — executable jar)
<!-- webnest-shop/pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.webnest</groupId>
<artifactId>webnest-shop</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>shop-domain</module>
<module>shop-persistence</module>
<module>shop-web</module>
<module>shop-app</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-web</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
(Each module has its own pom.xml whose <parent> is webnest-shop.)
Library module and application module POMs
<!-- shop-persistence/pom.xml -->
<parent>
<groupId>com.webnest</groupId>
<artifactId>webnest-shop</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>shop-persistence</artifactId>
<dependencies>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<!-- no spring-boot-maven-plugin here -->
<!-- shop-app/pom.xml -->
<artifactId>shop-app</artifactId>
<dependencies>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-web</artifactId>
</dependency>
<dependency>
<groupId>com.webnest</groupId>
<artifactId>shop-persistence</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
// shop-app/src/main/java/com/webnest/shop/ShopApplication.java
package com.webnest.shop; // root package shared by all modules -> scanning finds everything
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
mvn -q clean package
[INFO] webnest-shop ....................... SUCCESS
[INFO] shop-domain ........................ SUCCESS
[INFO] shop-persistence ................... SUCCESS
[INFO] shop-web ........................... SUCCESS
[INFO] shop-app ........................... SUCCESS
java -jar shop-app/target/shop-app-1.0.0.jar
... Started ShopApplication in 3.1 seconds
Useful build commands and the Gradle equivalent
# Build only the app module and the modules it needs
mvn -pl shop-app -am package
# Run the app module
mvn -pl shop-app spring-boot:run
// settings.gradle.kts
rootProject.name = "webnest-shop"
include("shop-domain", "shop-persistence", "shop-web", "shop-app")
// shop-app/build.gradle.kts
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
java
}
dependencies {
implementation(project(":shop-web"))
implementation(project(":shop-persistence"))
}
./gradlew :shop-app:bootRun
> Task :shop-app:bootRun
... Started ShopApplication in 2.9 seconds
Common Mistakes
- Applying spring-boot-maven-plugin to library modules, making their classes unusable by other modules.
- Creating circular dependencies between modules (web depends on persistence and persistence on web).
- Using different root packages per module and then wondering why beans, entities or repositories are not found.
- Splitting a small application into many modules too early, adding build complexity without benefit.
- Putting application.yml in a library module instead of the application module.
Key Points to Remember
- A parent POM with packaging pom lists modules and manages internal versions.
- Library modules are plain jars; only application modules use spring-boot-maven-plugin.
- Dependencies point one way: app → web → service/persistence → domain.
- Use a shared root package, or configure scanBasePackages, @EntityScan and @EnableJpaRepositories.
- Build selectively with mvn -pl <module> -am or Gradle project paths.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.