diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml
index 6dcc158..25ef1f2 100644
--- a/tapas-tasks/pom.xml
+++ b/tapas-tasks/pom.xml
@@ -62,6 +62,12 @@
mockito-core
2.21.0
+
+ com.tngtech.archunit
+ archunit-junit4
+ 0.22.0
+ test
+
org.springframework.boot
diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java
new file mode 100644
index 0000000..0d26f37
--- /dev/null
+++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java
@@ -0,0 +1,24 @@
+package ch.unisg.tapastasks;
+
+import ch.unisg.tapastasks.archunit.HexagonalArchitecture;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+
+class DependencyRuleTests {
+
+ @Test
+ void testPackageDependencies() {
+ noClasses()
+ .that()
+ .resideInAPackage("ch.unisg.tapastasks.tasks.domain..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("ch.unisg.tapastasks.tasks.application..")
+ .check(new ClassFileImporter()
+ .importPackages("ch.unisg.tapastasks.tasks.."));
+ }
+
+}
diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java
new file mode 100644
index 0000000..92a764b
--- /dev/null
+++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java
@@ -0,0 +1,62 @@
+package ch.unisg.tapastasks.archunit;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Adapters extends ArchitectureElement {
+
+ private final HexagonalArchitecture parentContext;
+ private List incomingAdapterPackages = new ArrayList<>();
+ private List outgoingAdapterPackages = new ArrayList<>();
+
+ Adapters(HexagonalArchitecture parentContext, String basePackage) {
+ super(basePackage);
+ this.parentContext = parentContext;
+ }
+
+ public Adapters outgoing(String packageName) {
+ this.incomingAdapterPackages.add(fullQualifiedPackage(packageName));
+ return this;
+ }
+
+ public Adapters incoming(String packageName) {
+ this.outgoingAdapterPackages.add(fullQualifiedPackage(packageName));
+ return this;
+ }
+
+ List allAdapterPackages() {
+ List allAdapters = new ArrayList<>();
+ allAdapters.addAll(incomingAdapterPackages);
+ allAdapters.addAll(outgoingAdapterPackages);
+ return allAdapters;
+ }
+
+ public HexagonalArchitecture and() {
+ return parentContext;
+ }
+
+ String getBasePackage() {
+ return basePackage;
+ }
+
+ void dontDependOnEachOther(JavaClasses classes) {
+ List allAdapters = allAdapterPackages();
+ for (String adapter1 : allAdapters) {
+ for (String adapter2 : allAdapters) {
+ if (!adapter1.equals(adapter2)) {
+ denyDependency(adapter1, adapter2, classes);
+ }
+ }
+ }
+ }
+
+ void doesNotDependOn(String packageName, JavaClasses classes) {
+ denyDependency(this.basePackage, packageName, classes);
+ }
+
+ void doesNotContainEmptyPackages() {
+ denyEmptyPackages(allAdapterPackages());
+ }
+}
diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java
new file mode 100644
index 0000000..e8e00a5
--- /dev/null
+++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java
@@ -0,0 +1,59 @@
+package ch.unisg.tapastasks.archunit;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ApplicationLayer extends ArchitectureElement {
+
+ private final HexagonalArchitecture parentContext;
+ private List incomingPortsPackages = new ArrayList<>();
+ private List outgoingPortsPackages = new ArrayList<>();
+ private List servicePackages = new ArrayList<>();
+
+ public ApplicationLayer(String basePackage, HexagonalArchitecture parentContext) {
+ super(basePackage);
+ this.parentContext = parentContext;
+ }
+
+ public ApplicationLayer incomingPorts(String packageName) {
+ this.incomingPortsPackages.add(fullQualifiedPackage(packageName));
+ return this;
+ }
+
+ public ApplicationLayer outgoingPorts(String packageName) {
+ this.outgoingPortsPackages.add(fullQualifiedPackage(packageName));
+ return this;
+ }
+
+ public ApplicationLayer services(String packageName) {
+ this.servicePackages.add(fullQualifiedPackage(packageName));
+ return this;
+ }
+
+ public HexagonalArchitecture and() {
+ return parentContext;
+ }
+
+ public void doesNotDependOn(String packageName, JavaClasses classes) {
+ denyDependency(this.basePackage, packageName, classes);
+ }
+
+ public void incomingAndOutgoingPortsDoNotDependOnEachOther(JavaClasses classes) {
+ denyAnyDependency(this.incomingPortsPackages, this.outgoingPortsPackages, classes);
+ denyAnyDependency(this.outgoingPortsPackages, this.incomingPortsPackages, classes);
+ }
+
+ private List allPackages() {
+ List allPackages = new ArrayList<>();
+ allPackages.addAll(incomingPortsPackages);
+ allPackages.addAll(outgoingPortsPackages);
+ allPackages.addAll(servicePackages);
+ return allPackages;
+ }
+
+ void doesNotContainEmptyPackages() {
+ denyEmptyPackages(allPackages());
+ }
+}
diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java
new file mode 100644
index 0000000..465f441
--- /dev/null
+++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java
@@ -0,0 +1,74 @@
+package ch.unisg.tapastasks.archunit;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+
+import java.util.List;
+
+import static com.tngtech.archunit.base.DescribedPredicate.*;
+import static com.tngtech.archunit.base.DescribedPredicate.greaterThanOrEqualTo;
+import static com.tngtech.archunit.lang.conditions.ArchConditions.*;
+import static com.tngtech.archunit.lang.conditions.ArchConditions.containNumberOfElements;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+
+abstract class ArchitectureElement {
+
+ final String basePackage;
+
+ public ArchitectureElement(String basePackage) {
+ this.basePackage = basePackage;
+ }
+
+ String fullQualifiedPackage(String relativePackage) {
+ return this.basePackage + "." + relativePackage;
+ }
+
+ static void denyDependency(String fromPackageName, String toPackageName, JavaClasses classes) {
+ noClasses()
+ .that()
+ .resideInAPackage("ch.unisg.tapastasks.tasks.domain..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("ch.unisg.tapastasks.tasks.application..")
+ .check(classes);
+ }
+
+ static void denyAnyDependency(
+ List fromPackages, List toPackages, JavaClasses classes) {
+ for (String fromPackage : fromPackages) {
+ for (String toPackage : toPackages) {
+ noClasses()
+ .that()
+ .resideInAPackage(matchAllClassesInPackage(fromPackage))
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage(matchAllClassesInPackage(toPackage))
+ .check(classes);
+ }
+ }
+ }
+
+ static String matchAllClassesInPackage(String packageName) {
+ return packageName + "..";
+ }
+
+ void denyEmptyPackage(String packageName) {
+ classes()
+ .that()
+ .resideInAPackage(matchAllClassesInPackage(packageName))
+ .should(containNumberOfElements(greaterThanOrEqualTo(1)))
+ .check(classesInPackage(packageName));
+ }
+
+ private JavaClasses classesInPackage(String packageName) {
+ return new ClassFileImporter().importPackages(packageName);
+ }
+
+ void denyEmptyPackages(List packages) {
+ for (String packageName : packages) {
+ denyEmptyPackage(packageName);
+ }
+ }
+}
diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java
new file mode 100644
index 0000000..1445968
--- /dev/null
+++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java
@@ -0,0 +1,61 @@
+package ch.unisg.tapastasks.archunit;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class HexagonalArchitecture extends ArchitectureElement {
+
+ private Adapters adapters;
+ private ApplicationLayer applicationLayer;
+ private String configurationPackage;
+ private List domainPackages = new ArrayList<>();
+
+ public static HexagonalArchitecture boundedContext(String basePackage) {
+ return new HexagonalArchitecture(basePackage);
+ }
+
+ public HexagonalArchitecture(String basePackage) {
+ super(basePackage);
+ }
+
+ public Adapters withAdaptersLayer(String adaptersPackage) {
+ this.adapters = new Adapters(this, fullQualifiedPackage(adaptersPackage));
+ return this.adapters;
+ }
+
+ public HexagonalArchitecture withDomainLayer(String domainPackage) {
+ this.domainPackages.add(fullQualifiedPackage(domainPackage));
+ return this;
+ }
+
+ public ApplicationLayer withApplicationLayer(String applicationPackage) {
+ this.applicationLayer = new ApplicationLayer(fullQualifiedPackage(applicationPackage), this);
+ return this.applicationLayer;
+ }
+
+ public HexagonalArchitecture withConfiguration(String packageName) {
+ this.configurationPackage = fullQualifiedPackage(packageName);
+ return this;
+ }
+
+ private void domainDoesNotDependOnOtherPackages(JavaClasses classes) {
+ denyAnyDependency(
+ this.domainPackages, Collections.singletonList(adapters.basePackage), classes);
+ denyAnyDependency(
+ this.domainPackages, Collections.singletonList(applicationLayer.basePackage), classes);
+ }
+
+ public void check(JavaClasses classes) {
+ this.adapters.doesNotContainEmptyPackages();
+ this.adapters.dontDependOnEachOther(classes);
+ this.adapters.doesNotDependOn(this.configurationPackage, classes);
+ this.applicationLayer.doesNotContainEmptyPackages();
+ this.applicationLayer.doesNotDependOn(this.adapters.getBasePackage(), classes);
+ this.applicationLayer.doesNotDependOn(this.configurationPackage, classes);
+ this.applicationLayer.incomingAndOutgoingPortsDoNotDependOnEachOther(classes);
+ this.domainDoesNotDependOnOtherPackages(classes);
+ }
+}