NetArchTest for Go? GoArchTest: Preserve Your Architecture 🏗️
June 13th, 2025 ¿Ves algún error? Corregir artículo
Have you ever wondered why your Go code compiles perfectly but your architecture deteriorates over time? The Go compiler prevents import cycles, but it cannot detect more subtle architectural violations. This is where GoArchTest comes in, a library that allows you to define and enforce architectural rules in your Go projects.
In this post, I'll show you how to use GoArchTest to keep your Go code organized and architecturally correct, especially when working with patterns like Clean Architecture and Domain-Driven Design.
Why do you need GoArchTest if Go already prevents import cycles?
That's the million-dollar question. While Go prevents this:
package_a.go// ❌ Go compiler ERROR: import cycle package A import "B" // A → B package B import "A" // B → A (ERROR: import cycle)
Go DOES allow this (but violates Clean Architecture):
domain.go// ✅ Go compiler: Compiles fine // ❌ Clean Architecture: VIOLATION package domain import "infrastructure" // Inner layer depending on outer layer package infrastructure import "domain" // This is correct in Clean Architecture
GoArchTest fills this gap by detecting architectural violations that Go cannot verify.
Installation and initial setup
First, install GoArchTest in your project:
~go get github.com/solrac97gr/goarchtest
1. Basic example: Layer separation
Let's start with a simple example where we want to ensure that the presentation layer doesn't directly depend on the data layer:
architecture_test.gopackage main_test import ( "testing" "path/filepath" "github.com/solrac97gr/goarchtest" ) func TestArchitecture(t *testing.T) { // Get project path projectPath, _ := filepath.Abs("./") // Create GoArchTest instance types := goarchtest.InPath(projectPath) // Rule: presentation should not depend on data result := types. That(). ResideInNamespace("presentation"). ShouldNot(). HaveDependencyOn("data"). GetResult() if !result.IsSuccessful { t.Error("❌ Violation: Presentation layer depends on data") for _, failingType := range result.FailingTypes { t.Logf("Violation in: %s (%s)", failingType.Name, failingType.Package) } } }
2. Validating complete Clean Architecture
GoArchTest includes predefined patterns for common architectures. Here's how to validate Clean Architecture:
clean_arch_test.gofunc TestCleanArchitecture(t *testing.T) { projectPath, _ := filepath.Abs("./") types := goarchtest.InPath(projectPath) // Define Clean Architecture pattern cleanArchPattern := goarchtest.CleanArchitecture( "domain", // Domain layer "application", // Application layer "infrastructure", // Infrastructure layer "presentation", // Presentation layer ) // Validate all rules validationResults := cleanArchPattern.Validate(types) // Check results passedRules := 0 for i, result := range validationResults { if result.IsSuccessful { passedRules++ t.Logf("✅ Rule #%d: SUCCESS", i+1) } else { t.Errorf("❌ Rule #%d: FAILURE", i+1) for _, failingType := range result.FailingTypes { t.Logf(" Violation: %s (%s)", failingType.Name, failingType.Package) } } } t.Logf("Summary: %d/%d rules passed", passedRules, len(validationResults)) }
3. Domain-Driven Design with Clean Architecture
For more complex projects using DDD, GoArchTest supports multiple bounded contexts:
ddd_test.gofunc TestDDDArchitecture(t *testing.T) { projectPath, _ := filepath.Abs("./") types := goarchtest.InPath(projectPath) // Define domains (bounded contexts) domains := []string{"user", "products", "orders"} // DDD pattern with Clean Architecture dddPattern := goarchtest.DDDWithCleanArchitecture( domains, // List of domains "internal/shared", // Shared kernel "pkg", // Utilities ) validationResults := dddPattern.Validate(types) // Verify isolation between domains for i, result := range validationResults { if !result.IsSuccessful { t.Errorf("❌ DDD Rule #%d fails", i+1) for _, failingType := range result.FailingTypes { t.Logf("Violation: %s (%s)", failingType.Name, failingType.Package) } } } }
Typical DDD project structure:
4. Custom rules for your project
You can create specific rules for your project using custom predicates:
custom_rules_test.gofunc TestCustomRules(t *testing.T) { projectPath, _ := filepath.Abs("./") types := goarchtest.InPath(projectPath) // Custom rule: Services must end with "Service" isServiceImplementation := func(typeInfo *goarchtest.TypeInfo) bool { return typeInfo.IsStruct && len(typeInfo.Name) > 7 && typeInfo.Name[len(typeInfo.Name)-7:] == "Service" } result := types. That(). WithCustomPredicate("IsServiceImplementation", isServiceImplementation). Should(). ResideInNamespace("application"). GetResult() if !result.IsSuccessful { t.Error("❌ Services must be in the application layer") } }
5. Dependency visualization
GoArchTest can generate dependency graphs to visualize your architecture:
graph_generation.gofunc GenerateDependencyGraph() { projectPath, _ := filepath.Abs("./") types := goarchtest.InPath(projectPath) // Create reporter reporter := goarchtest.NewErrorReporter(os.Stderr) // Get all types allTypes := types.That().GetAllTypes() // Generate DOT graph err := reporter.SaveDependencyGraph(allTypes, "dependency_graph.dot") if err != nil { log.Printf("Error generating graph: %v", err) return } log.Println("Graph saved at: dependency_graph.dot") log.Println("To generate PNG: dot -Tpng dependency_graph.dot -o dependency_graph.png") }
6. CI/CD integration
To automatically maintain your clean architecture, add the tests to your pipeline:
.github/workflows/architecture.ymlname: Architecture Tests on: [push, pull_request] jobs: architecture: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: 1.21 - name: Run Architecture Tests run: | go test -v ./tests/architecture/ - name: Generate Dependency Graph run: | go run cmd/graph/main.go - name: Upload Architecture Report uses: actions/upload-artifact@v3 with: name: architecture-report path: dependency_graph.png
Real benefits for development teams
🚀 Prevention of architectural deterioration
- Detects violations before they reach production
- Maintains consistency across different developers
- Avoids architectural "technical debt"
📚 Living documentation
- Tests serve as executable documentation
- New developers understand the architecture by reading the tests
- Rules are always up to date
🔧 Scalability
- Facilitates domain division into microservices
- Maintains clear boundaries between contexts
- Reduces coupling between components
Comparison: Go vs GoArchTest
🔧 Go Compiler
- ✅ Circular dependencies: Automatically prevents import cycles
- ❌ Layer violations: Allows any layer to depend on any other
- ❌ Dependency direction: Doesn't control architectural flow
- ❌ Domain isolation: No rules to separate business contexts
- ❌ Team consistency: Manual and error-prone validation
🏗️ GoArchTest
- 🟡 Circular dependencies: Not necessary (Go already handles it)
- ✅ Layer violations: Detects and prevents architectural violations
- ✅ Dependency direction: Controls flow according to Clean Architecture
- ✅ Domain isolation: Configurable for Domain-Driven Design
- ✅ Team consistency: Automated through executable tests
Conclusion
GoArchTest is an essential tool for any Go project that wants to maintain a solid and scalable architecture. It doesn't replace compiler protections, but complements them with architectural validations that go beyond what Go can verify.
By implementing GoArchTest in your project, you'll achieve:
- Consistent architecture across the team
- Early detection of architectural violations
- Executable documentation of your design decisions
- Solid foundation to scale your application
Do you already use any tool to validate architecture in your Go projects? Tell me in the comments how you keep your code organized!
💡 Extra tip: Combine GoArchTest with static analysis tools like golangci-lint for complete validation of your Go code.
🔗 Useful links: