Multi-Module Projects
- Parent POM-Structure
- Architecture & Modules
- Module POM-Structure
- Application Module POM-Structure
- Priming Build
- Enhancing
A multi-module project consists of multiple directories, each with its own POM file and source directory. The modules can depend on each other; Maven builds them in the correct order.
Important
| Even though the project consists of many modules, it is still a single application and is packaged into a single JAR file or WAR file. |
Multi-module projects are more complex than single-module projects, but offer a stricter separation of concerns in return. This makes them useful for large codebases, and projects with large teams. By splitting the code into smaller modules and controlling the dependencies among them, you reduce the risk of code misuse. Instead of relying on developer discipline alone, the compiler complains if you try to use a class in the wrong place.
Unlike single-module projects, there is no starter for creating a multi-module Maven project for a Vaadin application. Instead, you have to assemble the project without a starter, beginning with the parent POM file.
Note
| If you’ve never used multi-module Maven projects, you should at least read the Guide to Working with Multiple Modules before continuing. |
Parent POM-Structure
The parent POM file resides in the root directory of the project. In a Vaadin project, it serves two roles. First, it acts as the parent to all other modules, allowing its children to inherit dependencies and other configuration from it. Second, it acts as the reactor of the entire project, responsible for building the individual modules in the correct order. The parent module doesn’t contain any source code, nor should it be packaged into a JAR file.
To turn a basic Maven POM file into a parent, start by changing its packaging to pom
, like this:
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
The next steps are similar to the ones needed in single-module projects. Import the spring-boot-starter-parent
project like this:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version> 1
<relativePath/>
</parent>
-
You can check for the latest version on the MVN Repository.
Declare all other dependency versions as project properties, like so:
<properties>
<java.version>21</java.version> 1
<vaadin.version>24.5.8</vaadin.version> 2
</properties>
-
This property is used by
spring-boot-starter-parent
to configure the Java compiler plugin. -
You can check for the latest version on the MVN Repository.
Then import the Vaadin BOM, like this:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
To be able to use basic Spring features such as dependency injection, all modules in the project have to import the spring-context
dependency. To avoid having to explicitly declare it in every module, add it to the parent POM, like this:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependencies>
Unlike the single-module project, this POM file doesn’t contain any plugins. Instead, it contains a section that lists all of the modules that should be included in the project build. Since at this point you won’t have created any modules, add an empty section:
<modules>
</modules>
Below is how a fully configured POM file for an empty multi-module Vaadin application looks:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<vaadin.version>24.5.8</vaadin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependencies>
<modules>
</modules>
</project>
Architecture & Modules
The code structure of a project should resemble the architecture of the application. For multi-module projects, the project structure should also resemble the architecture of the application. Therefore, you’ll need to have a sense of how the architecture is going to look before assembling a multi-module Vaadin project.
Consider a fictional Vaadin application with a Views component, a Services component, and an Entities component.
These components correspond to the following Java packages:
-
com.example.application.views
-
com.example.application.services
-
com.example.application.entities
Additionally, you often have the following utility packages:
-
com.example.application.utils
-
com.example.application.security
To split these packages into separate Maven modules, you’ll need to look at their dependencies. They’re depicted by arrows in the diagram. Each module forms its own compilation unit and has its own classpath.
Following this principle, you might have a project structure that looks like this:
(root)
├── entities/
│ ├── src/main/java/com/example/application/entities/
│ │ └── ...
│ └── pom.xml
├── services/
│ ├── src/main/java/com/example/application/services/
│ │ └── ...
│ └── pom.xml
├── utils/
│ ├── src/main/java/com/example/application/utils/
│ │ └── ...
│ └── pom.xml
├── views/
│ ├── src/main/java/com/example/application/views/
│ │ └── ...
│ └── pom.xml
└── pom.xml
You may notice that the security
module is missing. This is intentional. It’ll be explained later.
Module POM-Structure
Every Maven module has its own POM file. They’re all quite similar. They all start with a reference to the parent POM:
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
Since all of the modules are part of the same application, they should have the same groupId
and version
. These are inherited from the parent POM, unless declared explicitly in the POM file. Because of this, you should omit the <groupId>
and <version>
elements from the module POM files.
Each module still needs an artifactId
. Use the same name for both the directory of a module and its artifactId
.
After this, you’ll need to declare the dependencies of the modules. These are the first differences.
The entities
module only depends on utils
, and any external dependencies it needs to access the database. The complete POM file looks 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>entities</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId> 1
<artifactId>utils</artifactId>
<version>${project.version}</version> 2
</dependency>
... 3
</dependencies>
</project>
-
Instead of writing
com.example.application
, you can use the Maven built-in propertyproject.groupId
. -
Instead of writing
1.0-SNAPSHOT
, you can use the Maven built-in propertyproject.version
. -
This is where you would add dependencies to Spring Data, Hibernate, JPA, JOOQ, the JDBC driver and so on.
The services
module depends on entities
and utils
. Because entities
already depends on utils
, services
gets it as a transitive dependency by only depending on entities
. However, for clarity, it is recommended to include the utils
dependency anyway. The complete POM file looks 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>services</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>entities</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>utils</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
The utils
module doesn’t have any dependencies at the beginning. Its POM file looks 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>utils</artifactId>
</project>
The views
module depends on Vaadin, the services
module and the utils
module. In addition, the views
module includes the Vaadin Maven plugin. The complete POM file looks 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>views</artifactId>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>services</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>utils</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-frontend</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>production</id>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
<exclusions>
<exclusion>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<goals>
<goal>build-frontend</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Whenever you add a new module to a project, remember also to declare it in the parent POM file, like this:
...
<modules>
<module>entities</module>
<module>views</module>
<module>services</module>
<module>utils</module>
</modules>
...
At this point, the project contains modules for all system components. It compiles, but you can’t actually run the application. This is because you haven’t added an Application
class, nor have you set up the Spring Boot Maven plugin. To do this, you’ll need to create another Maven module: the application module.
Application Module POM-Structure
The application module acts as the aggregator of a project. It brings all of the modules together and builds them into a self-contained executable JAR-file that you can deploy to production. Because of this, the application module is also sometimes referred to as the deployment unit of the project.
The application module is an ordinary Maven module that contains at least the Application
class, and the application’s configuration files. It imports all other modules, either explicitly or transitively, and adds the Spring Boot Maven plugin. The complete POM-file looks 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.application</groupId>
<artifactId>parent-pom</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>views</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Including the application module, the entire project structure now looks like this:
(root)
├── app/
│ ├── src/main/java/com/example/application/
│ │ └── ...
│ └── pom.xml
├── entities/
│ ├── src/main/java/com/example/application/entities/
│ │ └── ...
│ └── pom.xml
├── services/
│ ├── src/main/java/com/example/application/services/
│ │ └── ...
│ └── pom.xml
├── utils/
│ ├── src/main/java/com/example/application/utils/
│ │ └── ...
│ └── pom.xml
├── views/
│ ├── src/main/java/com/example/application/views/
│ │ └── ...
│ └── pom.xml
└── pom.xml
Remember the com.example.application.security
utility package still didn’t have a Maven module of its own. If this package only contains code that isn’t intended to be used by other modules, such as the Spring Security configuration, you can put it into the application module. You would then have a module structure like this:
app/
├── src/main/
│ ├── java/
│ │ └── com/example/application/
│ │ ├── security/
│ │ │ └── SecurityConfiguration.java
│ │ └── Application.java
│ └── resources/
│ └── application.properties
└── pom.xml
However, if your security
package also includes code that is intended to be used by other modules, you should create a separate Security module and put everything there.
Priming Build
The first-ever build of a Maven project is called the priming build. During this build, all of the dependencies are downloaded and the plugins are executed for the first time. When you work with a Vaadin multi-module project, it’s important to run the priming build either before, or directly after importing the project into your IDE. To perform a priming build, run this command in the root of the project:
$ mvn package
During the priming build, the Vaadin Maven plugin generates several frontend files that are needed when the application runs. Because the plugin is configured in the views
module, the files are also generated into that module. When you start the application, Vaadin finds these files and loads them, and any other frontend files you may have created, from the views
module.
Without the priming build, Vaadin would generate the missing files when the application starts for the first time. However, these files would be in the app
module and not in the views
module. Vaadin would also look for any other frontend files, like the theme, in the app
module. As a result, the application wouldn’t work properly.
Enhancing
Mapping system components and utility packages to Maven modules is only one way of structuring a multi-module Vaadin project. Depending on what concerns you want to separate, you can go further by splitting the application into more Maven modules. The following sections provide some examples of common scenarios.
Hiding Entities from Presentation Layer
Sometimes you might not want the presentation layer to interact directly with the entities. Instead, you may want to use Data Transfer Objects (DTO) to communicate with your application layer. To do this, split the Services component into two Maven modules: services-api
and services-impl
.
Put the service interfaces and DTOs in the services-api
module, and add it as a dependency to the views
module. The services-api
module shouldn’t depend on the entities
module. Otherwise, the entities
module becomes a transitive dependency of views
— something to avoid in this case.
Next, put the classes that implement the service interfaces in the service-impl
module. Annotate these classes with @Service
, or declare them as Spring beans in some other way. This module should depend on both the service-api
and the entities
module.
Finally, add both service-api
and services-impl
as dependencies to the application module. When the application starts, Spring instantiates the service classes and injects their instances into the views.
Supporting Pluggable Implementations
Sometimes you want to be able to plug in different implementations of a Service Provider Interface (SPI). For example, you may want to store and retrieve the entities from a local database, or from a remote REST service. To do this, create one Maven module for the SPI and one Maven module for each implementation:
Each module that implements the SPI should have a dependency on the SPI module. All of the modules that use the SPI should also have a dependency on it. The main difference is in the Java code: the client modules call the SPI, whereas the implementation modules implement the SPI. Annotate the implementation classes with @Service
, or declare them as Spring beans in some other way.
Even though you have multiple implementations of the same SPI, you can use only one of them at a time while the application is running. An alternative is to add a Maven profile for each implementation to your application module. Each profile would then include a dependency on the corresponding implementation module, like this:
<profiles>
<profile>
<id>app-with-first-implementation</id>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>first-implementation</artifactId>
<version>${project.version}
</dependency>
</dependencies>
</profile>
<profile>
<id>app-with-second-implementation</id>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>second-implementation</artifactId>
<version>${project.version}
</dependency>
</dependencies>
</profile>
</profiles>
You can now build different versions of your application. To build a package that contains the first implementation, you would run this command:
$ mvn package -P app-with-first-implementation
Likewise, to build a package that contains the second implementation, you would run this command:
$ mvn package -P app-with-second-implementation
When the application starts, Spring instantiates the classes of the included implementation module, and injects their instances wherever they’re needed.