README
¶
go-arch-guard
Architecture guardrails for Go projects via go test.
Define isolation, layer-direction, structure, naming, and blast-radius rules, then fail regular tests when the project shape drifts. Ships with DDD, Clean Architecture, Layered, Hexagonal, and Modular Monolith presets, and supports fully custom architecture models. No CLI to learn. No separate config format. Just Go tests.
Why
Architecture usually decays through a few broad mistakes, not through deep theoretical violations:
- cross-domain imports
- hidden composition roots
- package placement drift
- naming that breaks the intended project shape
go-arch-guard catches those coarse mistakes early via static analysis. It does not try to model every semantic nuance inside Go packages, and if Go already rejects something by itself (such as import cycles), that is not a primary target here.
Install
go get github.com/NamhaeSusan/go-arch-guard
Quick Start
DDD (default)
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", ""))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", ""))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure("."))
})
t.Run("blast radius", func(t *testing.T) {
report.AssertNoViolations(t, rules.AnalyzeBlastRadius(pkgs, "", ""))
})
}
Clean Architecture
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
m := rules.CleanArch()
opts := []rules.Option{rules.WithModel(m)}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", "", opts...))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", "", opts...))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs, opts...))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure(".", opts...))
})
}
Layered (Spring-style)
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
m := rules.Layered()
opts := []rules.Option{rules.WithModel(m)}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", "", opts...))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", "", opts...))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs, opts...))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure(".", opts...))
})
}
Hexagonal (Ports & Adapters)
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
m := rules.Hexagonal()
opts := []rules.Option{rules.WithModel(m)}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", "", opts...))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", "", opts...))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs, opts...))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure(".", opts...))
})
}
Modular Monolith
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
m := rules.ModularMonolith()
opts := []rules.Option{rules.WithModel(m)}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", "", opts...))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", "", opts...))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs, opts...))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure(".", opts...))
})
}
Custom Model
m := rules.NewModel(
rules.WithDomainDir("module"),
rules.WithSharedDir("lib"),
rules.WithSublayers([]string{"api", "logic", "data"}),
rules.WithDirection(map[string][]string{
"api": {"logic"},
"logic": {"data"},
"data": {},
}),
)
opts := []rules.Option{rules.WithModel(m)}
Run:
go test -run TestArchitecture -v
Sample output when violations exist:
=== RUN TestArchitecture/domain_isolation
[ERROR] violation: domain "order" must not import domain "user"
(file: internal/domain/order/app/service.go:5,
rule: isolation.cross-domain,
fix: use orchestration/ for cross-domain orchestration or move shared types to pkg/)
--- FAIL: TestArchitecture/domain_isolation
Pass empty strings for module and root to auto-extract from loaded packages. If the module cannot be determined, a meta.no-matching-packages warning is emitted.
Architecture Models
Built-in Presets
| Preset | Sublayers | Direction | Alias Required | Model Required |
|---|---|---|---|---|
DDD() |
handler, app, core/model, core/repo, core/svc, event, infra | handler→app→core/*, infra→core/repo | Yes | Yes |
CleanArch() |
handler, usecase, entity, gateway, infra | handler→usecase→entity+gateway, infra→gateway | No | No |
Layered() |
handler, service, repository, model | handler→service→repository+model, repository→model | No | No |
Hexagonal() |
handler, usecase, port, domain, adapter | handler→usecase→port+domain, adapter→port+domain | No | No |
ModularMonolith() |
api, application, domain, infrastructure | api→application→domain, infrastructure→domain | No | No |
DDD Layout
internal/
├── domain/
│ └── order/
│ ├── alias.go # public surface (required)
│ ├── handler/http/ # inbound adapters
│ ├── app/ # application service
│ ├── core/
│ │ ├── model/ # domain model (required)
│ │ ├── repo/ # repository interface
│ │ └── svc/ # domain service interface
│ ├── event/ # domain events
│ └── infra/persistence/ # outbound adapters
├── orchestration/ # cross-domain coordination
└── pkg/ # shared utilities
DDD layer direction:
| from | allowed to import |
|---|---|
handler |
app |
app |
core/model, core/repo, core/svc, event |
core |
core/model |
core/model |
nothing |
core/repo |
core/model |
core/svc |
core/model |
event |
core/model |
infra |
core/repo, core/model, event |
Clean Architecture Layout
internal/
├── domain/
│ └── product/
│ ├── handler/ # interface adapters (controllers)
│ ├── usecase/ # application business rules
│ ├── entity/ # enterprise business rules
│ ├── gateway/ # data access interfaces
│ └── infra/ # frameworks & drivers
├── orchestration/
└── pkg/
Clean Architecture layer direction:
| from | allowed to import |
|---|---|
handler |
usecase |
usecase |
entity, gateway |
entity |
nothing |
gateway |
entity |
infra |
gateway, entity |
Layered (Spring-style) Layout
internal/
├── domain/
│ └── order/
│ ├── handler/ # HTTP/gRPC handlers
│ ├── service/ # business logic
│ ├── repository/ # data access
│ └── model/ # domain models
├── orchestration/
└── pkg/
Layered direction:
| from | allowed to import |
|---|---|
handler |
service |
service |
repository, model |
repository |
model |
model |
nothing |
Hexagonal (Ports & Adapters) Layout
internal/
├── domain/
│ └── order/
│ ├── handler/ # driving adapters (HTTP, gRPC)
│ ├── usecase/ # application logic
│ ├── port/ # interfaces (inbound + outbound)
│ ├── domain/ # entities, value objects
│ └── adapter/ # driven adapters (DB, messaging)
├── orchestration/
└── pkg/
Hexagonal direction:
| from | allowed to import |
|---|---|
handler |
usecase |
usecase |
port, domain |
port |
domain |
domain |
nothing |
adapter |
port, domain |
Modular Monolith Layout
internal/
├── domain/
│ └── order/
│ ├── api/ # module public interface
│ ├── application/ # use cases
│ ├── domain/ # entities, value objects
│ └── infrastructure/ # DB, external services
├── orchestration/
└── pkg/
Modular Monolith direction:
| from | allowed to import |
|---|---|
api |
application |
application |
domain |
domain |
nothing |
infrastructure |
domain |
Custom Model
Start from DDD defaults and override what you need:
m := rules.NewModel(
rules.WithDomainDir("module"), // internal/module/ instead of internal/domain/
rules.WithOrchestrationDir("workflow"), // internal/workflow/
rules.WithSharedDir("lib"), // internal/lib/
rules.WithSublayers([]string{"api", "logic", "data"}),
rules.WithDirection(map[string][]string{
"api": {"logic"},
"logic": {"data"},
"data": {},
}),
rules.WithRequireAlias(false),
rules.WithRequireModel(false),
)
All model options:
| Option | Description |
|---|---|
WithSublayers([]string{...}) |
recognized sublayer names |
WithDirection(map[string][]string{...}) |
allowed import direction matrix |
WithPkgRestricted(map[string]bool{...}) |
sublayers that must not import shared pkg |
WithDomainDir("domain") |
top-level directory name for domains |
WithOrchestrationDir("orchestration") |
top-level directory name for orchestration |
WithSharedDir("pkg") |
top-level directory name for shared packages |
WithRequireAlias(bool) |
whether domain roots must define alias.go |
WithAliasFileName("alias.go") |
name of the alias file |
WithRequireModel(bool) |
whether domains must have a model directory |
WithModelPath("core/model") |
path to domain model directory |
WithDTOAllowedLayers([]string{...}) |
sublayers where DTOs are allowed |
WithBannedPkgNames([]string{...}) |
package names banned under internal/ |
WithLegacyPkgNames([]string{...}) |
package names that trigger migration warnings |
WithLayerDirNames(map[string]bool{...}) |
directory names considered "layer-like" for naming checks |
Isolation Rules
rules.CheckDomainIsolation(pkgs, module, root, opts...)
Blocks cross-domain imports and forces external access through domain root packages.
| Rule | Meaning |
|---|---|
isolation.cross-domain |
domain A must not import domain B |
isolation.cmd-deep-import |
cmd/ must only import domain roots, not sub-packages |
isolation.orchestration-deep-import |
orchestration must only import domain roots |
isolation.pkg-imports-domain |
shared pkg must not import any domain |
isolation.pkg-imports-orchestration |
shared pkg must not import orchestration |
isolation.domain-imports-orchestration |
domains must not import orchestration |
isolation.internal-imports-orchestration |
non-cmd/orchestration packages must not import orchestration |
isolation.internal-imports-domain |
unregistered internal packages must not import domains |
Import matrix:
| from → to | domain root | domain sub-pkg | orchestration | shared pkg |
|---|---|---|---|---|
| same domain | Yes | Yes | No | Yes |
| other domain | No | No | No | Yes |
| orchestration | Yes | No | Yes | Yes |
| cmd | Yes | No | Yes | Yes |
| shared pkg | No | No | No | Yes |
Layer Direction Rules
rules.CheckLayerDirection(pkgs, module, root, opts...)
Enforces allowed intra-domain dependency direction. The direction matrix is defined by the architecture model.
| Rule | Meaning |
|---|---|
layer.direction |
import violates the allowed layer direction |
layer.inner-imports-pkg |
inner layer imports shared pkg (controlled by PkgRestricted) |
layer.unknown-sublayer |
unknown sublayer found in domain |
Notes:
- same-sublayer imports are always allowed
- the domain root package is not checked
- direction matrix is fully customizable via
WithDirection
Structure Rules
rules.CheckStructure(root, opts...)
| Rule | Meaning |
|---|---|
structure.internal-top-level |
only allowed top-level packages under internal/ |
structure.banned-package |
banned package names (default: util, common, misc, helper, shared, services) |
structure.legacy-package |
legacy packages that should be migrated |
structure.misplaced-layer |
app/handler/infra outside domain slices |
structure.middleware-placement |
middleware/ must live in shared pkg |
structure.domain-root-alias-required |
domain root must define alias file (DDD only) |
structure.domain-root-alias-package |
alias file package name must match directory |
structure.domain-root-alias-only |
domain root may only contain alias file |
structure.domain-alias-no-interface |
alias file must not re-export interfaces |
structure.domain-model-required |
domain must have model directory (DDD only) |
structure.dto-placement |
DTO files only in handler/app |
Naming Rules
rules.CheckNaming(pkgs, opts...)
| Rule | Meaning |
|---|---|
naming.no-stutter |
exported type repeats the package name |
naming.no-impl-suffix |
exported type ends with Impl |
naming.snake-case-file |
file name is not snake_case |
naming.repo-file-interface |
file in repo/ lacks matching interface |
naming.no-layer-suffix |
file name redundantly repeats the layer name |
naming.domain-interface-repo-only |
domain interface outside repo sublayer (DDD only) |
naming.no-handmock |
test file defines hand-rolled mock/fake/stub |
Blast Radius
rules.AnalyzeBlastRadius(pkgs, module, root, opts...)
Surfaces internal packages with abnormally high coupling via IQR-based statistical outlier detection. Default severity is Warning. Skips projects with fewer than 5 internal packages.
| Rule | Meaning |
|---|---|
blast-radius.high-coupling |
package has statistically outlying transitive dependents |
| Metric | Definition |
|---|---|
| Ca (Afferent Coupling) | packages that import this package |
| Ce (Efferent Coupling) | packages this package imports |
| Instability | Ce / (Ca + Ce) |
| Transitive Dependents | full reverse-reachable set via BFS |
Options
Severity
// Log violations without failing the test
rules.CheckDomainIsolation(pkgs, "", "", rules.WithSeverity(rules.Warning))
Exclude Paths
// Skip subtrees during migration
rules.CheckDomainIsolation(pkgs, "", "",
rules.WithExclude("internal/legacy/..."),
)
Patterns are project-relative paths with forward slashes. ... matches the root and all descendants.
TUI Viewer
Visualize your project's package structure and dependencies in an interactive terminal UI.
go run github.com/NamhaeSusan/go-arch-guard/cmd/tui .
Features: health-status tree coloring, imports/reverse dependencies/coupling metrics, violation details, search/filter (/), keyboard navigation.
API Reference
| Function | Description |
|---|---|
analyzer.Load(dir, patterns...) |
load Go packages for analysis |
rules.CheckDomainIsolation(pkgs, module, root, opts...) |
cross-domain boundary checks |
rules.CheckLayerDirection(pkgs, module, root, opts...) |
intra-domain direction checks |
rules.CheckNaming(pkgs, opts...) |
naming convention checks |
rules.CheckStructure(root, opts...) |
filesystem structure checks |
rules.AnalyzeBlastRadius(pkgs, module, root, opts...) |
coupling outlier detection |
report.AssertNoViolations(t, violations) |
fail test on Error violations |
rules.DDD() |
DDD architecture model (default) |
rules.CleanArch() |
Clean Architecture model |
rules.Layered() |
Spring-style layered model |
rules.Hexagonal() |
Ports & Adapters model |
rules.ModularMonolith() |
Module-based layered model |
rules.NewModel(opts...) |
custom model builder |
rules.WithModel(m) |
apply custom model to checks |
rules.WithSeverity(rules.Warning) |
downgrade to warnings |
rules.WithExclude("path/...") |
skip a subtree |
Claude Code Plugin
/plugin marketplace add NamhaeSusan/go-arch-guard
/plugin install go-arch-guard@go-arch-guard-marketplace
External Import Hygiene
go-arch-guard checks project-internal imports only. External dependency hygiene should be enforced via AI tool instructions and code review. See the DDD external import constraints for a copy-paste template.
License
MIT