ArchUnit: Architecture as Code

Every software project starts with a clean whiteboard diagram: “The Web Layer talks to Service, and Service talks to Data.”

Six months later, the code is a mess. Controllers are calling Repositories directly. Domain objects depend on HTTP libraries. Architecture Drift has set in.

ArchUnit solves this by allowing you to write unit tests that enforce architectural rules. If someone breaks the architecture, the build fails.

1. The Genesis: Architecture as Code

Manual code reviews are not enough. Reviewers get tired. They miss subtle dependency violations. ArchUnit treats your architecture as a testable artifact, just like your business logic.

2. Core Concepts

ArchUnit operates on compiled Java bytecode.

  1. Importer: Reads class files (.class) into a graph.
  2. Rules: Fluent API to define constraints (noClasses()...).
  3. Check: verifying the graph against the rules.

Java Implementation

```java import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchRule; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; class ArchitectureTest { @Test void servicesShouldNotDependOnControllers() { // 1. Import var classes = new ClassFileImporter().importPackages("com.myapp"); // 2. Define Rule ArchRule rule = noClasses() .that().resideInAPackage("..service..") .should().dependOnClassesThat().resideInAPackage("..controller.."); // 3. Check rule.check(classes); } } ```
```go // Go doesn't have a standard "ArchUnit" library, // but we can write a simple linter using "golang.org/x/tools/go/packages" func TestArchitecture(t *testing.T) { // Conceptual Example pkgs := loadPackages("./...") for _, pkg := range pkgs { if isServiceLayer(pkg) { for _, importPath := range pkg.Imports { if isControllerLayer(importPath) { t.Errorf("Violation: Service %s imports Controller %s", pkg.PkgPath, importPath) } } } } } ```

3. Hardware Reality: Static Analysis

ArchUnit is fast because it uses static analysis (Bytecode inspection) rather than reflection or runtime execution. It doesn’t load classes into the JVM ClassLoader. It parses the bytecode headers to build a dependency graph. This means it can analyze thousands of classes in milliseconds.

4. Common Patterns

Layered Architecture

Enforce the strict flow of control.

layeredArchitecture()
    .consideringAllDependencies()
    .layer("Web").definedBy("..web..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Web").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Web")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");

Cycle Detection

Cyclic dependencies (A → B → A) make code impossible to refactor.

slices().matching("com.myapp.(*)..")
    .should().beFreeOfCycles();

5. Interactive: Violation Visualizer

Drag the dependency arrow to see which connections are allowed.

Web
Service
Data
Select a dependency path to check.

6. Best Practices

  1. Fail Fast: Run ArchUnit tests as part of your normal unit test suite.
  2. Common Rules: Use GeneralCodingRules to ban standard anti-patterns (e.g., System.out.println, joda-time).
  3. Naming Conventions: Enforce that all implementations of Repository must end with Repository.