Production engineering guide for clean architecture patterns covering patterns, implementation strategies, and operational best practices.
Clean architecture is a structural approach that separates business logic from technical implementation details, enabling systems to remain maintainable and adaptable as requirements evolve. This pattern becomes essential when building systems where business rules must outlast technical decisions, or when teams need to swap components without disrupting core logic.
Business entities should contain only domain-specific data and behavior, with no dependencies on external frameworks or databases. The following example shows an entity that encapsulates business rules without technical concerns:
type User struct {
ID int
Name string
Email string
IsActive bool
}
func (u *User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
if u.Email == "" {
return errors.New("email is required")
}
return nil
}
Dependencies like databases or external services must be abstracted through interfaces defined in the core layer. These interfaces are implemented by infrastructure code:
type UserRepository interface {
Save(user *User) error
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
Infrastructure code depends on the core interfaces but provides concrete implementations. This layer handles database connections, HTTP clients, file systems, or external API integrations:
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Save(user *User) error {
_, err := r.db.Exec("INSERT INTO users (name, email, is_active) VALUES ($1, $2, $3)",
user.Name, user.Email, user.IsActive)
return err
}
The application layer orchestrates business operations using core entities and interfaces, without knowing how concrete implementations are provided:
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
user := &User{Name: name, Email: email, IsActive: true}
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.repo.Save(user); err != nil {
return nil, err
}
return user, nil
}
Dependencies flow inward from outer layers to inner layers. The core layer cannot depend on infrastructure or presentation:
Presentation Layer (HTTP handlers)
↓
Application Layer (Use cases, services)
↓
Core Layer (Entities, interfaces, business rules)
↓
Infrastructure Layer (Database drivers, external clients)
Data passed between layers should be explicitly defined to avoid coupling through implementation details:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
}
Avoid passing domain entities directly between layers. Convert them to appropriate DTOs:
func (s *UserService) HandleCreateUser(req CreateUserRequest) (*CreateUserResponse, error) {
user := &User{Name: req.Name, Email: req.Email, IsActive: true}
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.repo.Save(user); err != nil {
return nil, err
}
return &CreateUserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
IsActive: user.IsActive,
}, nil
}
Logging should not be part of business logic. Instead, use interfaces that can be implemented in infrastructure:
type Logger interface {
Info(message string, fields map[string]interface{})
Error(message string, err error, fields map[string]interface{})
}
Entities must remain free from technical concerns like database schema definitions or HTTP request objects:
// ❌ Wrong - contains database-specific field
type User struct {
ID int `db:"id"`
Name string
Email string
IsActive bool
}
// ✅ Correct - clean business entity
type User struct {
ID int
Name string
Email string
IsActive bool
}
A common error occurs when business rules depend on database connection details or framework-specific features:
// ❌ Wrong - business logic depends on SQL
func (u *User) UpdateStatus(db *sql.DB, status bool) error {
_, err := db.Exec("UPDATE users SET is_active = ? WHERE id = ?",
status, u.ID)
return err
}
When infrastructure code directly references concrete types instead of interfaces:
// ❌ Wrong - dependency on concrete type
type UserService struct {
db *sql.DB // Direct dependency on database driver
}
// ✅ Correct - dependency on interface
type UserService struct {
repo UserRepository // Dependency on interface
}
Sometimes developers place infrastructure concerns in the core layer:
// ❌ Wrong - database-specific code in core
func (u *User) SaveToDatabase() error {
db, err := sql.Open("postgres", "connection_string")
if err != nil {
return err
}
_, err = db.Exec("INSERT INTO users (...) VALUES (...)")
return err
}
// ✅ Correct - core logic only
func (u *User) Validate() error {
// Validation logic only
}
The dependency inversion principle ensures that abstractions do not depend on concrete implementations:
// Interface defined in core layer
type PaymentProcessor interface {
Process(amount float64) (string, error)
}
// Implementation in infrastructure layer
type StripePaymentProcessor struct{}
func (p *StripePaymentProcessor) Process(amount float64) (string, error) {
// Stripe-specific implementation
return "payment_id", nil
}
Ensure that layers depend only on inner layers:
// ❌ Wrong - circular dependency
type User struct {
NotificationService *NotificationService // Depends on presentation layer
}
// ✅ Correct - no circular dependencies
type User struct {
ID int
Name string
Email string
}
Core entities should not contain methods that directly interact with external systems:
// ❌ Wrong - entity knows about HTTP
func (u *User) SendEmail() error {
client := &http.Client{}
req, _ := http.NewRequest("POST", "https://api.example.com/send",
strings.NewReader("email content"))
_, err := client.Do(req)
return err
}
// ✅ Correct - entity stays pure
func (u *User) GetEmail() string {
return u.Email
}
When domain entities are used directly in API responses without proper conversion, business logic may be silently bypassed:
// ❌ Risky - direct entity usage
func (s *UserService) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
return user, nil // Returns domain entity directly
}
// ✅ Safer - explicit conversion
func (s *UserService) GetUser(id int) (*UserResponse, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
return &UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
IsActive: user.IsActive,
}, nil
}
When interfaces are implemented incorrectly or incompletely, systems may silently fail:
// ❌ Missing method implementation
type UserRepository interface {
Save(user *User) error
FindByID(id int) (*User, error)
// Missing FindByEmail method
}
// ✅ Complete interface
type UserRepository interface {
Save(user *User) error
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
Sometimes dependencies appear to be in the correct layer but are actually indirect:
// ❌ Indirect dependency - uses framework-specific type
func (s *UserService) ProcessPayment(req PaymentRequest) error {
// PaymentRequest contains framework-specific types
// This makes the service dependent on HTTP framework
return nil
}
When tests directly access infrastructure instead of using interfaces, they lose their ability to verify core business logic:
// ❌ Test depends on concrete implementation
func TestUserService(t *testing.T) {
repo := &PostgresUserRepository{} // Direct dependency
service := NewUserService(repo)
// Test is now tied to database implementation
}
// ✅ Test uses interface
func TestUserService(t *testing.T) {
repo := &MockUserRepository{} // Interface-based mock
service := NewUserService(repo)
// Test can verify business logic without infrastructure
}
Use static analysis or build tools to ensure that outer layers do not depend on inner layers. Tools like go vet or custom linters can help enforce these boundaries.
Regularly review interface implementations to ensure all required methods are properly implemented and tested.
Each layer should have a single, well-defined responsibility. The core layer handles business logic, the application layer orchestrates operations, and infrastructure handles technical concerns.
The clean architecture pattern provides a reliable foundation for building systems that can evolve over time without requiring complete rewrites of business logic. The key is maintaining strict boundaries between layers and ensuring that dependencies flow in the correct direction.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy