Maven describes a build in pom.xml. A module owns a responsibility and can depend on other modules. Ports/adapters protects domain rules from direct dependencies on HTTP, SQL or an interface. PDF Batch Studio has a framework-free core and different adapters. Sharing the core makes sense within this one product. It is not a shared library for all AppFabrik apps.
01Build a Maven project
Install Maven and check mvn --version with JDK 21. Save pom.xml and src/main/java/tutorial/Main.java in a new folder. package compiles, tests and produces a JAR. The project repositories also include a Maven wrapper.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>learning</artifactId><version>1.0</version>
<properties><maven.compiler.release>21</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies></dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.3</version></plugin>
</plugins></build>
</project>
Run / check
mvn package dependency:copy-dependencies
java -cp "target/classes:target/dependency/*" tutorial.Main02Make direction visible
The service knows only Store. MemoryStore implements that port. main wires them together. In the real project, the core does not depend on the Spring controller or JavaFX interface. Those adapters depend on the core.
package tutorial;
import java.util.ArrayList;
import java.util.List;
public class Main {
interface Store { void save(String value); }
static final class MemoryStore implements Store {
final List<String> values = new ArrayList<>();
public void save(String value) { values.add(value); }
}
static final class Service {
final Store store;
Service(Store store) { this.store = store; }
void create(String input) {
if (input == null || input.isBlank()) throw new IllegalArgumentException("Name required");
store.save(input.trim());
}
}
public static void main(String[] args) {
var store = new MemoryStore();
new Service(store).create(" Letters ");
System.out.println(store.values);
}
}
03Check modules selectively
In the PDF repository, ./mvnw -pl core/pdf-batch-core -am test checks the core and required preceding modules. -pl selects modules and -am includes their dependencies. A core test needs no running interface.
04Justify abstractions
The port is useful because desktop, server and tests need different storage. An interface for every trivial helper method would not provide comparable value. Architecture should facilitate a concrete replacement or boundary.
The small build produces a stored learning value. The domain rule remains independent of the storage implementation.
Your exercise
Replace MemoryStore with an adapter writing a text file. Which class should handle IOException?
Show the solution and explanation
The file adapter handles the technical IOException and translates it according to the port contract. The service should not depend on java.nio or a file path. Explicitly decide whether the port returns a failure or uses a defined exception.
Project context and sources
The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.