How to add new modules, endpoints, services, dependencies, and quality rules to this project.
See also: Architecture Patterns · Best Practices · Toolchain
The project uses a multi-module layout under examples/. Each module has its own pom.xml that inherits from the root.
examples/my-module/
├── pom.xml
└── src/
├── main/java/com/example/template/mymodule/
└── test/java/com/example/template/mymodule/
<?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>com.example.template</groupId>
<artifactId>java-enterprise-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>template-my-module</artifactId>
<name>My Module</name>
<description>Brief description of what this module demonstrates</description>
<dependencies>
<!-- Add module-specific dependencies here.
Common deps (SLF4J, JUnit, AssertJ, Mockito) are inherited from the parent. -->
</dependencies>
</project>Add the module to the <modules> block:
<modules>
<!-- existing modules -->
<module>examples/my-module</module>
</modules>./mvnw compile -pl examples/my-moduleThis follows the pattern in examples/restful-api/. The layers are: Controller → Service → Domain, with DTOs at the boundary.
package com.example.template.restfulapi.domain;
public class Order {
private UUID id;
private String customerName;
private BigDecimal total;
// constructor, getters, setters
}package com.example.template.restfulapi.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
public record OrderRequest(
@NotBlank String customerName,
@Positive BigDecimal total
) {}package com.example.template.restfulapi.dto;
import java.math.BigDecimal;
import java.util.UUID;
public record OrderResponse(UUID id, String customerName, BigDecimal total) {}package com.example.template.restfulapi.service;
public interface OrderService {
List<Order> findAll();
Order findById(UUID id);
Order create(OrderRequest request);
}package com.example.template.restfulapi.service;
import org.springframework.stereotype.Service;
@Service
public class InMemoryOrderService implements OrderService {
private final Map<UUID, Order> store = new ConcurrentHashMap<>();
@Override
public List<Order> findAll() {
return List.copyOf(store.values());
}
@Override
public Order findById(UUID id) {
var order = store.get(id);
if (order == null) {
throw new ResourceNotFoundException("Order", id);
}
return order;
}
@Override
public Order create(OrderRequest request) {
var order = new Order(UUID.randomUUID(), request.customerName(), request.total());
store.put(order.getId(), order);
return order;
}
}package com.example.template.restfulapi.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/orders")
@Tag(name = "Orders", description = "Order operations")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping
public List<OrderResponse> list() {
return orderService.findAll().stream()
.map(o -> new OrderResponse(o.getId(), o.getCustomerName(), o.getTotal()))
.toList();
}
@PostMapping
public ResponseEntity<OrderResponse> create(@Valid @RequestBody OrderRequest request) {
var order = orderService.create(request);
return ResponseEntity
.created(URI.create("/api/v1/orders/" + order.getId()))
.body(new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal()));
}
}Key conventions:
- Controllers never expose domain objects directly — use DTOs
- Use
@Validon request bodies for bean validation - Return
201 Createdwith aLocationheader for POST - Add OpenAPI annotations (
@Operation,@ApiResponse,@Tag) for Swagger docs
Services that don't need a REST layer follow the same interface + implementation pattern.
// Interface
package com.example.template.mymodule.service;
public interface NotificationService {
void send(String recipient, String message);
}// Implementation — Spring discovers this via component scanning
package com.example.template.mymodule.service;
import org.springframework.stereotype.Service;
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
// implementation
}
}Inject via constructor (no @Autowired needed when there's a single constructor):
public class OrderController {
private final OrderService orderService;
private final NotificationService notificationService;
public OrderController(OrderService orderService, NotificationService notificationService) {
this.orderService = orderService;
this.notificationService = notificationService;
}
}This is the preferred approach — it keeps versions consistent across all modules.
1. Add to <dependencyManagement> in the root pom.xml:
<properties>
<caffeine.version>3.1.8</caffeine.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
</dependencies>
</dependencyManagement>2. Use in the child module (no version needed):
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>For dependencies only one module needs, declare the version directly in the child pom.xml:
<dependency>
<groupId>org.example</groupId>
<artifactId>niche-library</artifactId>
<version>1.0.0</version>
</dependency>Run the OWASP dependency check to verify no known vulnerabilities:
./mvnw verify -Psecurity-scan -pl examples/my-moduleThe project uses four static analysis tools. Each has a config file under the config/ directory.
| File | Purpose |
|---|---|
config/checkstyle/checkstyle.xml |
Rule definitions |
config/checkstyle/checkstyle-suppressions.xml |
Suppressions for specific files/rules |
Add a suppression (e.g., allow long methods in a specific file):
<!-- config/checkstyle/checkstyle-suppressions.xml -->
<suppress files="MyLegacyClass\.java" checks="MethodLength"/>Suppress inline with a comment:
@SuppressWarnings("checkstyle:MagicNumber")
private static final int TIMEOUT = 30;| File | Purpose |
|---|---|
config/pmd/pmd-ruleset.xml |
Rule inclusions/exclusions |
Exclude a rule globally:
<rule ref="category/java/bestpractices.xml">
<exclude name="AvoidReassigningParameters"/>
</rule>Suppress inline:
@SuppressWarnings("PMD.ShortVariable")
int x = computeValue();| File | Purpose |
|---|---|
config/spotbugs/spotbugs-exclude.xml |
Exclusion filter |
Suppress a specific bug pattern on a class:
<Match>
<Class name="com.example.template.mymodule.MyClass"/>
<Bug pattern="EI_EXPOSE_REP"/>
</Match>| File | Purpose |
|---|---|
config/owasp/owasp-suppressions.xml |
CVE false-positive suppressions |
Suppress a false-positive CVE:
<suppress>
<notes><![CDATA[False positive — we don't use the affected feature. Reviewed 2024-03-15.]]></notes>
<gav regex="true">^com\.example:my-library:.*$</gav>
<cve>CVE-2024-XXXXX</cve>
</suppress>Always include a <notes> element explaining why the suppression is safe and when it was reviewed.
# Full scan (SpotBugs + Checkstyle + PMD + OWASP)
./mvnw verify -Psecurity-scan
# Quick scan (no OWASP — faster for local dev)
./mvnw verify -Psecurity-scan-quick
# Single tool
./mvnw checkstyle:check
./mvnw pmd:check
./mvnw spotbugs:check- Architecture Patterns — project structure and layer design
- Best Practices — coding standards and conventions
- Security Scanning — detailed security tool configuration
- Third-Party Libraries — dependency catalog and selection criteria
- Toolchain — tool installation and IDE setup
- Development Workflow — branching, CI, and release process