Multi-Module Maven Projects: Building Enterprise Java Applications

Multi-module Maven projects allow you to organize large codebases into smaller, manageable modules with clear dependencies and responsibilities. This approach promotes code reuse, separation of concerns, and easier maintenance.

Project Structure Overview

A typical multi-module Maven project has this structure:

my-enterprise-app/
├── pom.xml (Parent POM)
├── core/
│   ├── pom.xml
│   └── src/
├── service/
│   ├── pom.xml
│   └── src/
├── web/
│   ├── pom.xml
│   └── src/
└── api/
├── pom.xml
└── src/

Parent POM Configuration

Example 1: Parent POM (pom.xml)

<?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</groupId>
<artifactId>my-enterprise-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>My Enterprise Application</name>
<description>Multi-module enterprise Java application</description>
<!-- Module declarations -->
<modules>
<module>core</module>
<module>service</module>
<module>web</module>
<module>api</module>
</modules>
<!-- Properties -->
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<spring-boot.version>3.1.0</spring-boot.version>
<junit.version>5.9.2</junit.version>
</properties>
<!-- Dependency Management -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Common dependencies with versions -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Build Configuration -->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<!-- Common plugins for all modules -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- Profiles -->
<profiles>
<profile>
<id>dev</id>
<properties>
<build.profile.id>dev</build.profile.id>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<build.profile.id>prod</build.profile.id>
</properties>
</profile>
</profiles>
</project>

Core Module Implementation

Example 2: Core Module (core/pom.xml)

<?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</groupId>
<artifactId>my-enterprise-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>core</artifactId>
<packaging>jar</packaging>
<name>Core Module</name>
<description>Core domain models and business logic</description>
<dependencies>
<!-- Internal dependencies -->
<!-- External dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Example 3: Core Domain Models

// core/src/main/java/com/example/core/domain/User.java
package com.example.core.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(min = 2, max = 50)
@Column(name = "username", unique = true, nullable = false)
private String username;
@NotBlank
@Email
@Column(name = "email", unique = true, nullable = false)
private String email;
@NotBlank
@Size(min = 6)
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
private Role role;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Constructors
public User() {
this.createdAt = LocalDateTime.now();
}
public User(String username, String email, String passwordHash, Role role) {
this();
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
// Business methods
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
public boolean isAdmin() {
return Role.ADMIN.equals(this.role);
}
// equals, hashCode, toString
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(id, user.id) && 
Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(id, username);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", role=" + role +
'}';
}
}
// core/src/main/java/com/example/core/domain/Role.java
package com.example.core.domain;
public enum Role {
USER, ADMIN, MODERATOR
}

Example 4: Core Service Interfaces

// core/src/main/java/com/example/core/service/UserService.java
package com.example.core.service;
import com.example.core.domain.User;
import com.example.core.exception.UserNotFoundException;
import java.util.List;
import java.util.Optional;
public interface UserService {
User createUser(User user);
User updateUser(Long userId, User user) throws UserNotFoundException;
void deleteUser(Long userId) throws UserNotFoundException;
Optional<User> getUserById(Long userId);
Optional<User> getUserByUsername(String username);
Optional<User> getUserByEmail(String email);
List<User> getAllUsers();
List<User> getUsersByRole(String role);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
// core/src/main/java/com/example/core/exception/UserNotFoundException.java
package com.example.core.exception;
public class UserNotFoundException extends Exception {
public UserNotFoundException(String message) {
super(message);
}
public UserNotFoundException(Long userId) {
super("User not found with ID: " + userId);
}
}

Service Module Implementation

Example 5: Service Module (service/pom.xml)

<?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</groupId>
<artifactId>my-enterprise-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>service</artifactId>
<packaging>jar</packaging>
<name>Service Module</name>
<description>Business logic and service implementations</description>
<dependencies>
<!-- Internal dependencies -->
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- External dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Example 6: Service Implementation

// service/src/main/java/com/example/service/impl/UserServiceImpl.java
package com.example.service.impl;
import com.example.core.domain.User;
import com.example.core.domain.Role;
import com.example.core.exception.UserNotFoundException;
import com.example.core.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Autowired
public UserServiceImpl(UserRepository userRepository, 
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public User createUser(User user) {
// Validate business rules
if (existsByUsername(user.getUsername())) {
throw new IllegalArgumentException("Username already exists: " + user.getUsername());
}
if (existsByEmail(user.getEmail())) {
throw new IllegalArgumentException("Email already exists: " + user.getEmail());
}
// Encode password
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
// Set default role if not provided
if (user.getRole() == null) {
user.setRole(Role.USER);
}
return userRepository.save(user);
}
@Override
public User updateUser(Long userId, User userDetails) throws UserNotFoundException {
User existingUser = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
// Update allowed fields
if (userDetails.getEmail() != null && 
!userDetails.getEmail().equals(existingUser.getEmail())) {
if (existsByEmail(userDetails.getEmail())) {
throw new IllegalArgumentException("Email already exists: " + userDetails.getEmail());
}
existingUser.setEmail(userDetails.getEmail());
}
if (userDetails.getRole() != null) {
existingUser.setRole(userDetails.getRole());
}
return userRepository.save(existingUser);
}
@Override
public void deleteUser(Long userId) throws UserNotFoundException {
if (!userRepository.existsById(userId)) {
throw new UserNotFoundException(userId);
}
userRepository.deleteById(userId);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserById(Long userId) {
return userRepository.findById(userId);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
@Override
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
@Transactional(readOnly = true)
public List<User> getUsersByRole(String role) {
return userRepository.findByRole(Role.valueOf(role.toUpperCase()));
}
@Override
@Transactional(readOnly = true)
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}
@Override
@Transactional(readOnly = true)
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}
}
// service/src/main/java/com/example/service/repository/UserRepository.java
package com.example.service.repository;
import com.example.core.domain.User;
import com.example.core.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByRole(Role role);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchUsers(@Param("keyword") String keyword);
}

Web Module Implementation

Example 7: Web Module (web/pom.xml)

<?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</groupId>
<artifactId>my-enterprise-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>web</artifactId>
<packaging>jar</packaging>
<name>Web Module</name>
<description>Web controllers and REST endpoints</description>
<dependencies>
<!-- Internal dependencies -->
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.web.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Example 8: Web Controllers and DTOs

// web/src/main/java/com/example/web/dto/UserDTO.java
package com.example.web.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserDTO {
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 2, max = 50, message = "Username must be between 2 and 50 characters")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
private String role;
// Constructors
public UserDTO() {}
public UserDTO(Long id, String username, String email, String role) {
this.id = id;
this.username = username;
this.email = email;
this.role = role;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
// web/src/main/java/com/example/web/controller/UserController.java
package com.example.web.controller;
import com.example.core.domain.User;
import com.example.core.exception.UserNotFoundException;
import com.example.core.service.UserService;
import com.example.web.dto.UserDTO;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
User user = convertToEntity(userDTO);
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(convertToDTO(createdUser));
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(user -> ResponseEntity.ok(convertToDTO(user)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> users = userService.getAllUsers().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(users);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id, 
@Valid @RequestBody UserDTO userDTO) {
try {
User userDetails = convertToEntity(userDTO);
User updatedUser = userService.updateUser(id, userDetails);
return ResponseEntity.ok(convertToDTO(updatedUser));
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
// Conversion methods
private User convertToEntity(UserDTO userDTO) {
User user = new User();
user.setUsername(userDTO.getUsername());
user.setEmail(userDTO.getEmail());
user.setPasswordHash(userDTO.getPassword()); // Will be encoded in service
if (userDTO.getRole() != null) {
user.setRole(com.example.core.domain.Role.valueOf(userDTO.getRole().toUpperCase()));
}
return user;
}
private UserDTO convertToDTO(User user) {
return new UserDTO(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getRole().name()
);
}
}
// web/src/main/java/com/example/web/Application.java
package com.example.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.example.core", "com.example.service", "com.example.web"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

API Module Implementation

Example 9: API Module (api/pom.xml)

<?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</groupId>
<artifactId>my-enterprise-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>api</artifactId>
<packaging>jar</packaging>
<name>API Module</name>
<description>API models and client libraries</description>
<dependencies>
<!-- Internal dependencies -->
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- External dependencies -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.8</version>
</dependency>
</dependencies>
</project>

Example 10: API Models and Documentation

// api/src/main/java/com/example/api/model/ApiResponse.java
package com.example.api.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "Standard API response wrapper")
public class ApiResponse<T> {
@Schema(description = "Response status", example = "success")
private String status;
@Schema(description = "Response message", example = "Operation completed successfully")
private String message;
@Schema(description = "Response data")
private T data;
@Schema(description = "Error code if applicable")
private String errorCode;
// Constructors
public ApiResponse() {}
public ApiResponse(String status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
// Factory methods
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>("success", "Operation completed successfully", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>("success", message, data);
}
public static <T> ApiResponse<T> error(String message) {
ApiResponse<T> response = new ApiResponse<>();
response.status = "error";
response.message = message;
return response;
}
public static <T> ApiResponse<T> error(String message, String errorCode) {
ApiResponse<T> response = new ApiResponse<>();
response.status = "error";
response.message = message;
response.errorCode = errorCode;
return response;
}
// Getters and setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
}

Testing in Multi-Module Projects

Example 11: Integration Tests

// web/src/test/java/com/example/web/controller/UserControllerTest.java
package com.example.web.controller;
import com.example.core.domain.User;
import com.example.core.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@Test
void createUser_ValidUser_ReturnsCreated() throws Exception {
// Given
User user = new User("testuser", "[email protected]", "password", User.Role.USER);
user.setId(1L);
when(userService.createUser(any(User.class))).thenReturn(user);
String userJson = """
{
"username": "testuser",
"email": "[email protected]",
"password": "password123",
"role": "USER"
}
""";
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void getUserById_UserExists_ReturnsUser() throws Exception {
// Given
User user = new User("testuser", "[email protected]", "password", User.Role.USER);
user.setId(1L);
when(userService.getUserById(1L)).thenReturn(Optional.of(user));
// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("testuser"));
}
}

Build and Deployment Configuration

Example 12: Maven Build Profiles

<!-- In parent pom.xml -->
<profiles>
<profile>
<id>dev</id>
<properties>
<build.profile.id>dev</build.profile.id>
<skip.integration.tests>true</skip.integration.tests>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>integration-test</id>
<properties>
<skip.unit.tests>true</skip.unit.tests>
<skip.integration.tests>false</skip.integration.tests>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>release</id>
<properties>
<build.profile.id>prod</build.profile.id>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

Best Practices for Multi-Module Projects

  1. Clear Module Boundaries: Each module should have a single responsibility
  2. Dependency Management: Use dependencyManagement in parent POM
  3. Circular Dependencies: Avoid circular dependencies between modules
  4. Version Management: Use properties for versions in parent POM
  5. Testing Strategy: Implement unit tests in each module, integration tests in appropriate modules
  6. Documentation: Document module responsibilities and dependencies
  7. Build Optimization: Use Maven reactor and build profiles effectively

Common Commands

# Build all modules
mvn clean install
# Build specific module
mvn clean install -pl web
# Build with dependencies
mvn clean install -pl web -am
# Skip tests
mvn clean install -DskipTests
# Build with profile
mvn clean install -P integration-test
# Build only changed modules
mvn clean install -pl $(mvn -q -N exec:exec -Dexec.executable="pwd")

Conclusion

Multi-module Maven projects provide an excellent structure for organizing large Java applications. Benefits include:

  • Modularity: Clear separation of concerns
  • Reusability: Modules can be reused across projects
  • Maintainability: Easier to understand and modify
  • Build Efficiency: Independent module compilation
  • Team Collaboration: Different teams can work on different modules

By following the patterns and best practices outlined above, you can create well-structured, maintainable, and scalable Java applications using multi-module Maven projects.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper