DEV Community

Bala Paranj
Bala Paranj

Posted on

One Test File That Prevents All Architecture Regressions

Every team talks about hexagonal architecture. Almost no team enforces it.

You draw the dependency diagram: core/ never imports adapters/. adapters/ never imports cmd/. Everyone nods. Then a developer in a hurry adds import "internal/adapters/output" inside a core package because they need a JSON helper. The code review approves it because the reviewer is focused on the feature logic, not the import list.

Six months later, core/ imports from 4 adapter packages and nobody remembers when it started.

I wrote one test file that makes this impossible.

The Test

func TestCoreNeverImportsAdapters(t *testing.T) {
    forbidden := []string{
        "github.com/myapp/internal/adapters/",
        "github.com/myapp/internal/cli/",
        "github.com/myapp/cmd/",
        "os/exec",
    }

    corePackages := discoverPackages(t, "internal/core/")

    for _, pkg := range corePackages {
        for _, imp := range pkg.Imports {
            for _, banned := range forbidden {
                if strings.HasPrefix(imp, banned) {
                    t.Errorf("core package %s imports forbidden %s", 
                        pkg.Name, imp)
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It walks every Go file in internal/core/, extracts the import list, and fails if any import matches a forbidden prefix. That's it.

What It Catches

Real violation: os/exec in core

When I added an E2E test runner to internal/core/enginetest/, the test immediately caught it:

core test isolation violation: internal/core/enginetest/e2e_test.go: imports os/exec
Enter fullscreen mode Exit fullscreen mode

os/exec is an infrastructure dependency — it invokes external processes. It doesn't belong in core, even in test files. The E2E runner was moved to e2e/ at the repo root.

Without this test, the import would have stayed. It compiled fine. No reviewer would have caught it because os/exec in a test file looks reasonable.

Real violation: adapter type in port interface

A port interface in app/contracts/ accepted an adapter type as a parameter:

// BEFORE: Port contaminated with adapter type
type EvaluationLoader interface {
    Load(ctx context.Context, path string) (*adapters.Evaluation, error)
}
Enter fullscreen mode Exit fullscreen mode

The test caught the import of adapters/ from the app/ layer. The fix: the port returns a domain type, and the adapter converts at the boundary.

// AFTER: Port uses only domain types
type EvaluationLoader interface {
    Load(ctx context.Context, path string) (*report.Assessment, error)
}
Enter fullscreen mode Exit fullscreen mode

Why Code Reviews Aren't Enough

Code reviews catch architecture violations about 80% of the time. The other 20%:

  • The reviewer is focused on the feature logic, not import lists
  • The PR has 40 files and the violation is in file 37
  • The import looks reasonable in context ("I just need this one helper")
  • The reviewer doesn't know the architecture rules because they're in a wiki nobody reads

A test catches violations 100% of the time. It runs in CI on every push. It doesn't get tired, distracted, or rushed.

How to Build It

Step 1: Define your layers

internal/
  core/       → NEVER imports adapters, app, cmd, os/exec
  app/        → NEVER imports adapters, cmd
  adapters/   → May import core, app
  cli/        → May import anything
cmd/          → May import anything
Enter fullscreen mode Exit fullscreen mode

Step 2: Discover packages programmatically

func discoverPackages(t *testing.T, root string) []PackageInfo {
    var packages []PackageInfo
    filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if !d.IsDir() && strings.HasSuffix(path, ".go") {
            imports := extractImports(path)
            packages = append(packages, PackageInfo{
                Path:    path,
                Imports: imports,
            })
        }
        return nil
    })
    return packages
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Check every import against the forbidden list

for _, pkg := range corePackages {
    for _, imp := range pkg.Imports {
        for _, banned := range forbidden {
            if strings.HasPrefix(imp, banned) {
                t.Errorf("%s imports forbidden %s", pkg.Path, imp)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Run it in CI

- name: Architecture check
  run: go test ./internal/app/ -run TestCoreNeverImportsAdapters
Enter fullscreen mode Exit fullscreen mode

The ROI

This test is the most leveraged single commit in the codebase.

Over 4 months and 60 refactorings, every structural change had to pass this test. When we merged internal/infra/ into internal/adapters/, the test verified no core packages imported the old path. When we moved types between layers, the test verified the dependency direction.

Without it, the hexagonal boundaries would have eroded under the pressure of daily development. With it, the architecture is mechanically guaranteed.

The test took 30 minutes to write. It has prevented more regressions than any refactoring in the catalog.

What It Doesn't Catch

  • Logic violations: A function in core/ that formats CLI output text. The import is legal (fmt), but the responsibility is wrong. This still needs code review.
  • Interface design: A port with 10 methods (ISP violation). The import direction is correct but the interface is too fat. This needs design review.
  • Naming: A core package named utils instead of a domain concept. The architecture is fine but discoverability suffers.

The fitness function enforces the structural rules. Design and naming still need human judgment. But by automating the structural check, you free up code review time to focus on the things that require judgment.


Checkout the code in my open source project Stave. The architecture fitness function has been running in CI since January 2026 and has caught 4 violations that would have been missed in code review.

Top comments (0)