Packages & Project Structure
Why Package Structure Is an Architectural Decision
In Go, packages are not just folders. They are the primary unit of encapsulation, compilation, and dependency management. Poor package structure does not fail immediately — it degrades slowly. Over time, it produces import cycles, fragile tests, and “god packages” that nobody wants to touch.
In production systems, package structure directly affects:
- Build stability
- Dependency direction
- Test isolation
- Refactor safety
- Onboarding speed
Real Production Failure: The Import Cycle Incident
A growing backend service had loosely defined packages: handlers imported services, services imported repositories, repositories imported logging utilities, and eventually logging utilities imported configuration that referenced service-level types.
The result:
import cycle not allowed
The team attempted quick fixes by moving functions between packages, creating a tangled graph that became harder to reason about. A full refactor was eventually required.
Lesson: dependency direction must be intentional from day one.
Baseline Production Layout
my-service/
go.mod
cmd/
api/
main.go
internal/
app/
domain/
repository/
transport/
pkg/
- cmd/ contains entrypoints only.
- internal/ contains application code not intended for external reuse.
- pkg/ is optional and should only contain intentionally exported libraries.
The internal/ Directory Rule
Go enforces visibility rules for the internal/ directory. Code inside internal/ cannot be imported from outside the module tree. This is a powerful architectural tool.
Use internal/ to prevent accidental external coupling.
Dependency Direction: High-Level Rule
Dependencies should point inward. Infrastructure depends on domain, not the other way around.
Correct Direction
- transport → service
- service → domain
- repository → domain
Incorrect Direction
- domain → repository
- service → transport
- domain → logging implementation
Interface Placement in Go
In Go, interfaces are implicitly satisfied. Best practice: define interfaces where they are consumed, not where they are implemented.
Bad Pattern
package repository
type UserRepository interface {
FindByID(id int) (*User, error)
}
This forces consumers to depend on repository-level abstractions.
Better Pattern
package service
type UserRepository interface {
FindByID(id int) (*User, error)
}
Now service defines what it needs. The repository package provides an implementation.
Avoiding Import Cycles
Detect Cycles Early
go build ./... go list -deps ./...
Common Causes
- Shared utility packages that depend on higher-level code
- Logging packages importing domain types
- Global configuration referenced everywhere
Cycle Prevention Strategies
- Extract shared types into neutral packages
- Move interfaces upward
- Separate pure domain logic from infrastructure
Domain vs Infrastructure Separation
Domain packages should not import database drivers, HTTP frameworks, or logging implementations.
Domain Package Should Contain
- Business rules
- Core entities
- Validation logic
Infrastructure Package Should Contain
- SQL implementations
- HTTP handlers
- External API clients
This separation increases testability and long-term stability.
Monorepo vs Multi-Module
Single Module (Common Case)
- Simpler dependency management
- Easier refactors
- Unified versioning
Multi-Module Setup
- Useful when multiple independent services exist
- Requires strict version management
- Increases cognitive overhead
Most production services should start with a single module unless there is a clear boundary requiring separation.
Testing and Package Design
Good package boundaries enable clean unit tests.
Example
package service_test
import (
"testing"
)
Using external test packages (service_test) ensures you test the public API, not internal details.
Common Testing Anti-Pattern
- Accessing unexported internals via same-package tests
- Using global variables to bypass boundaries
God Package Anti-Pattern
Symptoms:
- Huge package with dozens of files
- Everything imports it
- Refactors require touching many unrelated components
Fix:
- Split by responsibility, not by technical layer
- Avoid dumping shared helpers into one place
Refactoring Strategy in Growing Codebases
Step 1: Visualize Dependencies
go list -deps ./...
Step 2: Identify Cycles or Bidirectional Dependencies
Step 3: Introduce Interfaces at Boundaries
Step 4: Extract Shared Types
Refactoring packages is easier early. Delay increases cost exponentially.
Operational Checklist
- No import cycles
- Clear dependency direction (inward)
- Interfaces defined at consumption boundaries
- Domain free of infrastructure imports
- Entry points isolated under cmd/
- internal/ used intentionally
- No god packages
Final Perspective
Package structure is not about folder aesthetics. It defines the shape of your system. In Go, simplicity is powerful — but only when boundaries are intentional. Architecture mistakes in package design compound over time. Design dependency direction carefully, and your system will scale cleanly.