Add archunit module: ArchUnit for Spring Boot - enforcing layered architecture as a unit test

This commit is contained in:
Claude
2026-10-03 21:21:03 +00:00
parent 3d05e483cc
commit 8be1245951
17 changed files with 459 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# ArchUnit for Spring Boot — Enforcing Layered Architecture as a Unit Test
Companion module for [ArchUnit for Spring Boot: Enforcing Layered Architecture as a Unit
Test](https://ankurm.com/archunit-spring-boot-layered-architecture-unit-test/) on
[ankurm.com](https://ankurm.com). Every code sample and every console transcript quoted in
that post comes from the files in this directory — nothing was hand-typed into the article.
## Versions this was built and run against
| Component | Version | Notes |
|---|---|---|
| ArchUnit | **1.5.1** | current GA per `maven-metadata.xml` on Maven Central at the time of writing |
| JUnit Jupiter / Platform | **6.1.3** | via `archunit-junit5`, which brings its own JUnit 5 extension |
| Spring Framework | 7.0.9 | `spring-context` + `spring-webmvc` only — no `spring-boot-starter-web`, no embedded server, no `ApplicationContext` anywhere in this module |
| JDK | **25 (Temurin, LTS)** | build and run; `maven.compiler.release` is set to 17 |
| Maven | 3.9.11 | |
| Maven Surefire Plugin | 3.5.2 | |
This module deliberately does not depend on any `spring-boot-starter-*` artifact. ArchUnit
reads compiled `.class` files directly — it has never needed a running Spring context, and
the whole point of the first section of the post is that none of these tests say
`@SpringBootTest`.
## Quickstart
```bash
mvn test # all four rules, no application ever starts, ~4s total
mvn test -Dtest=LayeredArchitectureTest # just the three structural rules
mvn test -Dtest=FreezingArchRuleTest # just the frozen-violation rule
```
Gradle users: there is no `build.gradle.kts` in this module (unlike this repo's other
modules) because the whole demonstration is Maven-plugin-agnostic — `archunit-junit5`
plugs into the JUnit 5 engine the same way under either build tool, and the commands above
are the part that matters.
## Source files
| File | Demonstrates |
|---|---|
| [`OrderRepository.java`](src/main/java/com/ankurm/tutorials/junit/archunit/repository/OrderRepository.java) / [`InMemoryOrderRepository.java`](src/main/java/com/ankurm/tutorials/junit/archunit/repository/InMemoryOrderRepository.java) | the repository layer — a `@Repository` with no framework beyond the stereotype annotation itself |
| [`OrderStatusService.java`](src/main/java/com/ankurm/tutorials/junit/archunit/service/OrderStatusService.java) | the service layer — constructor injection only, which is what makes `noFieldInjection` meaningful rather than cosmetic |
| [`OrderStatusController.java`](src/main/java/com/ankurm/tutorials/junit/archunit/controller/OrderStatusController.java) | the web layer — depends on the service layer only |
| [`LegacyOrderExporter.java`](src/main/java/com/ankurm/tutorials/junit/archunit/service/LegacyOrderExporter.java) | a deliberately-kept pre-existing `throws Exception` violation — the thing `FreezingArchRuleTest` freezes rather than fixes |
| [`LayeredArchitectureTest.java`](src/test/java/com/ankurm/tutorials/junit/archunit/LayeredArchitectureTest.java) | `layeredArchitecture()`, `noFields().should().beAnnotatedWith(Autowired.class)`, and `slices().should().beFreeOfCycles()` — three independent structural rules, all checked without starting anything |
| [`FreezingArchRuleTest.java`](src/test/java/com/ankurm/tutorials/junit/archunit/FreezingArchRuleTest.java) | `FreezingArchRule.freeze(...)` wrapping a rule that would otherwise fail immediately, recording today's known violations as an accepted baseline |
| [`archunit.properties`](src/test/resources/archunit.properties) | the three properties that control where the frozen-violation store lives and whether it may be created or updated |
| [`archunit_store/`](archunit_store) | the frozen-violation store itself, committed to version control — this directory **is** the baseline the rule enforces against |
## Captured output
| File | What it shows |
|---|---|
| [`00-baseline-passing-run.txt`](docs/output/00-baseline-passing-run.txt) | the three structural rules, all green, ~1.3s, no `ApplicationContext` started |
| [`01-layering-violation-failure.txt`](docs/output/01-layering-violation-failure.txt) | a real `layeredArchitecture()` failure, captured by temporarily adding a controller that bypassed the service layer, then deleting it |
| [`02-freeze-run1-baseline-captured.txt`](docs/output/02-freeze-run1-baseline-captured.txt) | the first-ever run of `FreezingArchRuleTest`: no store existed yet, so it was created with today's one known violation already accepted |
| [`03-freeze-run2-new-violation-caught.txt`](docs/output/03-freeze-run2-new-violation-caught.txt) | a second, genuinely new `throws Exception` added after the baseline was recorded — the failure names only the new offender, never the frozen one |
| [`04-full-suite-final.txt`](docs/output/04-full-suite-final.txt) | all four rules, exactly as committed, green in under four seconds |
## A diagnostic note, not a defect
`LegacyOrderExporter.export()` still declares `throws Exception` in the committed code, and
`noNewGenericExceptionsDeclared` in `FreezingArchRuleTest` still technically covers it. The
build is green anyway, on purpose: `archunit_store/` records that specific violation as
already known at the time the rule was turned on, and `freeze()` only fails a build on a
violation that is *new* since that recording. This is the realistic case — a team adopting
ArchUnit on a codebase that is not already clean — and the point the post's freezing section
makes is that you do not have to choose between "fix two hundred existing violations today"
and "don't bother enforcing the rule at all."
## License
MIT, matching the rest of this repository.
@@ -0,0 +1 @@
Method <com.ankurm.tutorials.junit.archunit.service.LegacyOrderExporter.export()> does declare throwable of type java.lang.Exception in (LegacyOrderExporter.java:14)
+3
View File
@@ -0,0 +1,3 @@
#
#Sun Oct 04 02:49:04 IST 2026
no\ methods\ should\ declare\ throwable\ of\ type\ java.lang.Exception,\ because\ a\ specific\ exception\ type\ documents\ what\ a\ caller\ actually\ has\ to\ handle;\ `throws\ Exception`\ documents\ nothing=9e504a56-cf50-43d0-a977-82f8a097091b
@@ -0,0 +1,17 @@
Captured from a real `mvn test` run against the module as committed: three
ArchUnit rules (layered architecture, no field injection, no package cycles),
all passing, no Spring context started anywhere in the run.
[INFO] Running com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.311 s -- in com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[INFO] Results:
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
[INFO] Total time: 3.618 s
The whole suite -- three architecture rules checked across every compiled class
in the module -- finishes in about 1.3 seconds. There is no "Started
Application in N seconds" line anywhere in this output, because there is no
Application to start: @AnalyzeClasses reads .class files from disk, the same
way a decompiler would, and none of these three rules need a single bean to
exist at runtime to be checked.
@@ -0,0 +1,46 @@
Captured from a real `mvn test` run with a temporary class added to the controller
package, com.ankurm.tutorials.junit.archunit.controller.BadOrderLookupController,
whose constructor took OrderRepository directly instead of going through
OrderStatusService. The class was deleted immediately after this run; it exists
only in this transcript and in the "before/after" diagram in the published post.
[INFO] Running com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.445 s <<< FAILURE! -- in com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[ERROR] LayeredArchitectureTest.controllersOnlyCallServicesWhichOnlyCallRepositories -- Time elapsed: 1.402 s <<< FAILURE!
java.lang.AssertionError:
Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture considering all dependencies, consisting of
layer 'Controller' ('..controller..')
layer 'Service' ('..service..')
layer 'Repository' ('..repository..')
where layer 'Controller' may not be accessed by any layer
where layer 'Service' may only be accessed by layers ['Controller']
where layer 'Repository' may only be accessed by layers ['Service']' was violated (3 times):
Constructor <com.ankurm.tutorials.junit.archunit.controller.BadOrderLookupController.<init>(com.ankurm.tutorials.junit.archunit.repository.OrderRepository)> has parameter of type <com.ankurm.tutorials.junit.archunit.repository.OrderRepository> in (BadOrderLookupController.java:0)
Field <com.ankurm.tutorials.junit.archunit.controller.BadOrderLookupController.orderRepository> has type <com.ankurm.tutorials.junit.archunit.repository.OrderRepository> in (BadOrderLookupController.java:0)
Method <com.ankurm.tutorials.junit.archunit.controller.BadOrderLookupController.rawStatus(java.lang.String)> calls method <com.ankurm.tutorials.junit.archunit.repository.OrderRepository.findStatus(java.lang.String)> in (BadOrderLookupController.java:25)
at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:86)
at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:347)
at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:168)
at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:151)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
[INFO] Results:
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
Three things worth noticing in this message:
1. ArchUnit reports all THREE offending facts about the one bad class in a single
violation group: the constructor parameter, the field it gets assigned to, and
the actual method call that crosses the layer boundary. One bad wire gets
reported three ways because ArchUnit checks dependency *existence*, not just
call sites -- the field and the constructor parameter are both "access" on
their own, even on a line that never executes.
2. The file:line at the end of each fact (BadOrderLookupController.java:25) points
at real source positions ArchUnit extracted from the compiled .class file's
debug info, not from parsing the .java file.
3. "Priority: MEDIUM" is the rule's declared priority, not a severity the test
assigns at failure time -- layeredArchitecture() rules default to MEDIUM, and
a build can be configured to fail the whole run only above a given priority.
@@ -0,0 +1,33 @@
Captured from a real `mvn test -Dtest=FreezingArchRuleTest` run with no archunit_store/
directory present yet and freeze.store.default.allowStoreCreation=true in
src/test/resources/archunit.properties.
[INFO] Running com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.268 s -- in com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
The build is green even though LegacyOrderExporter.export() genuinely declares
`throws Exception` -- the one thing noMethods().should().declareThrowableOfType(Exception.class)
exists to ban. That's the point of freeze(): on this first run there was no stored
baseline yet, so FreezingArchRule recorded every violation it found right now as
"already known" and created the store on disk instead of failing. The store this
run produced, committed alongside this module:
archunit_store/stored.rules:
#
#Sun Oct 04 02:49:04 IST 2026
no\ methods\ should\ declare\ throwable\ of\ type\ java.lang.Exception,\ because\ a\ specific\
exception\ type\ documents\ what\ a\ caller\ actually\ has\ to\ handle;\ `throws\ Exception`\
documents\ nothing=9e504a56-cf50-43d0-a977-82f8a097091b
archunit_store/9e504a56-cf50-43d0-a977-82f8a097091b:
Method <com.ankurm.tutorials.junit.archunit.service.LegacyOrderExporter.export()> does
declare throwable of type java.lang.Exception in (LegacyOrderExporter.java:14)
stored.rules maps one line per frozen rule (keyed by the rule's full description) to a
UUID; that UUID is also a filename holding the exact violation text frozen under it, in
the same format the live failure message uses. Delete that second file and the rule
forgets the violation was ever acceptable -- which is exactly how you make a frozen
violation start failing again once someone finally fixes it.
@@ -0,0 +1,32 @@
Captured from a real `mvn test -Dtest=FreezingArchRuleTest` run, taken immediately after
the first run (docs/output/02-freeze-run1-baseline-captured.txt) had already frozen
LegacyOrderExporter.export() into archunit_store/. Between the two runs: a second class,
NewOrderBatchJob, was added with a method making the exact same mistake
(`run() throws Exception`), and freeze.store.default.allowStoreCreation was flipped to
false in archunit.properties -- the normal CI setting once a baseline exists, so nobody
can accidentally create a fresh, empty baseline that silently un-freezes everything.
[INFO] Running com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.156 s <<< FAILURE! -- in com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[ERROR] FreezingArchRuleTest.noNewGenericExceptionsDeclared -- Time elapsed: 1.145 s <<< FAILURE!
java.lang.AssertionError:
Architecture Violation [Priority: MEDIUM] - Rule 'no methods should declare throwable of type java.lang.Exception, because a specific exception type documents what a caller actually has to handle; `throws Exception` documents nothing' was violated (1 times):
Method <com.ankurm.tutorials.junit.archunit.service.NewOrderBatchJob.run()> does declare throwable of type java.lang.Exception in (NewOrderBatchJob.java:15)
at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:86)
at com.tngtech.archunit.library.freeze.FreezingArchRule.check(FreezingArchRule.java:97)
[INFO] Results:
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
This is the entire point of freeze(): the violation this test reports is ONLY
NewOrderBatchJob.run(). LegacyOrderExporter.export() still declares `throws Exception`
too -- nothing about it changed -- and it is not mentioned anywhere in this failure,
because it was already recorded as accepted in archunit_store/ before this run started.
A rule that would otherwise have to fail against every pre-existing offender at once
instead fails against exactly one thing: the offender that showed up after the team
agreed to stop adding new ones.
NewOrderBatchJob was deleted immediately after this run; it was never meant to survive
past this transcript. docs/output/00- and docs/output/02- show the suite back to green
with it removed and the frozen baseline still in place.
@@ -0,0 +1,17 @@
Captured from a real `mvn test` run against the module exactly as committed: all four
rules across both test classes, including the frozen rule with its one accepted legacy
violation still on record in archunit_store/.
[INFO] Running com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.323 s -- in com.ankurm.tutorials.junit.archunit.LayeredArchitectureTest
[INFO] Running com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.051 s -- in com.ankurm.tutorials.junit.archunit.FreezingArchRuleTest
[INFO] Results:
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
[INFO] Total time: 3.847 s
Four rules, four green results, under four seconds total, with zero application
classes started. This is the number to compare against docs/output/01- and
docs/output/03- -- both of those are the same suite with one extra class added on
purpose, each producing exactly one new failure and nothing else.
+69
View File
@@ -0,0 +1,69 @@
<?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.ankurTutorials</groupId>
<artifactId>archunit-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
<archunit.version>1.5.1</archunit.version>
<spring.version>7.0.9</spring.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Just enough Spring to annotate the sample app with real stereotypes
(@RestController, @Service, @Repository, @Autowired), not spring-boot-starter,
no embedded server, no ApplicationContext anywhere in this module. ArchUnit reads
compiled classes; it has never needed a running Spring context to do it. -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,22 @@
package com.ankurm.tutorials.junit.archunit.controller;
import com.ankurm.tutorials.junit.archunit.service.OrderStatusService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/** The web layer. Depends on the service layer only -- never on {@code repository} directly. */
@RestController
public class OrderStatusController {
private final OrderStatusService orderStatusService;
public OrderStatusController(OrderStatusService orderStatusService) {
this.orderStatusService = orderStatusService;
}
@GetMapping("/orders/{orderId}/status")
public String status(@PathVariable String orderId) {
return orderStatusService.statusOf(orderId);
}
}
@@ -0,0 +1,19 @@
package com.ankurm.tutorials.junit.archunit.repository;
import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Repository
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, String> statusesByOrderId = new ConcurrentHashMap<>(
Map.of("o-1", "SHIPPED", "o-2", "PLACED"));
@Override
public Optional<String> findStatus(String orderId) {
return Optional.ofNullable(statusesByOrderId.get(orderId));
}
}
@@ -0,0 +1,8 @@
package com.ankurm.tutorials.junit.archunit.repository;
import java.util.Optional;
/** The persistence boundary. Only a class in this package, or {@code service}, should ever call this. */
public interface OrderRepository {
Optional<String> findStatus(String orderId);
}
@@ -0,0 +1,15 @@
package com.ankurm.tutorials.junit.archunit.service;
/**
* Stands in for code that existed before anyone wired ArchUnit into this module: a method
* that declares {@code throws Exception} instead of something specific. Nobody is fixing this
* today, and a rule banning it outright would fail the build over a problem nobody signed up
* to solve right now. {@code FreezingArchRuleTest} freezes this one known violation so the
* rule can still be turned on -- see the module README for what "freezing" means here.
*/
public class LegacyOrderExporter {
public void export() throws Exception {
// Deliberately vague: this is the thing a real legacy codebase actually looks like.
}
}
@@ -0,0 +1,24 @@
package com.ankurm.tutorials.junit.archunit.service;
import com.ankurm.tutorials.junit.archunit.repository.OrderRepository;
import org.springframework.stereotype.Service;
/**
* The service layer. Constructor injection only -- no {@code @Autowired} field anywhere in
* this class. That's not a style preference being enforced here for its own sake; it's what
* makes {@code NoFieldInjectionTest} able to tell the difference between "this class declares
* its dependency" and "this class has one injected into it by magic after construction."
*/
@Service
public class OrderStatusService {
private final OrderRepository orderRepository;
public OrderStatusService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public String statusOf(String orderId) {
return orderRepository.findStatus(orderId).orElse("UNKNOWN");
}
}
@@ -0,0 +1,32 @@
package com.ankurm.tutorials.junit.archunit;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMethods;
import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze;
/**
* {@code noMethods().should().declareThrowableOfType(Exception.class)} is a rule most
* codebases fail the instant they turn it on, because {@code throws Exception} is scattered
* through whatever existed before anyone cared. Wrapping the rule in {@code freeze(...)}
* records today's violations as the accepted baseline (see {@code archunit_store/} next to
* this module's pom.xml -- that directory is committed, and it IS the baseline) and only
* fails the build on a violation that is NEW since that baseline was recorded.
*
* <p>{@code LegacyOrderExporter.export()} is the one violation this project already had when
* this rule was written. It is frozen. It will keep passing. Any new method anyone adds later
* that also declares {@code throws Exception} will fail this test on its own, without the
* frozen one ever needing to be fixed first. See {@code docs/output/02-*.txt} and
* {@code docs/output/03-*.txt} for two real runs proving exactly that.
*/
@AnalyzeClasses(packages = "com.ankurm.tutorials.junit.archunit")
class FreezingArchRuleTest {
@ArchTest
static final ArchRule noNewGenericExceptionsDeclared = freeze(
noMethods().should().declareThrowableOfType(Exception.class)
.because("a specific exception type documents what a caller actually "
+ "has to handle; `throws Exception` documents nothing"));
}
@@ -0,0 +1,42 @@
package com.ankurm.tutorials.junit.archunit;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.beans.factory.annotation.Autowired;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
/**
* No Spring context anywhere in this class. {@code @AnalyzeClasses} points ArchUnit at the
* compiled {@code .class} files under this package and these four rules run against them
* directly -- the whole suite finishes in well under a second.
*/
@AnalyzeClasses(packages = "com.ankurm.tutorials.junit.archunit")
class LayeredArchitectureTest {
@ArchTest
static final ArchRule controllersOnlyCallServicesWhichOnlyCallRepositories = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
@ArchTest
static final ArchRule noFieldInjection = noFields()
.that().areDeclaredInClassesThat().resideInAPackage("com.ankurm.tutorials.junit.archunit..")
.should().beAnnotatedWith(Autowired.class)
.because("constructor injection is what lets a test construct these classes with "
+ "plain `new`, and what makes a missing dependency a compile error instead "
+ "of a null field discovered at 2am");
@ArchTest
static final ArchRule packagesAreFreeOfCycles = slices()
.matching("com.ankurm.tutorials.junit.archunit.(*)..")
.should().beFreeOfCycles();
}
@@ -0,0 +1,6 @@
# Where FreezingArchRule's default TextFileBasedViolationStore keeps its frozen-violation
# records, and whether it's allowed to create/update that store. Commit the store directory
# itself to version control -- it IS the baseline this rule is enforcing against.
freeze.store.default.path=archunit_store
freeze.store.default.allowStoreCreation=false
freeze.store.default.allowStoreUpdate=true