Clean Architecture
Learn how Goca implements and enforces Clean Architecture principles from Robert C. Martin (Uncle Bob) in your Go projects.
What is Clean Architecture?
Clean Architecture is a software design philosophy that separates code into layers with clear responsibilities and dependencies that point inward toward the core business logic.
Core Principles
- Independence of Frameworks - Business logic doesn't depend on libraries
- Testability - Business rules can be tested without UI, database, or external services
- Independence of UI - Change UI without changing business logic
- Independence of Database - Swap databases without affecting business rules
- Independence of External Systems - Business logic knows nothing about the outside world
The Dependency Rule
Source code dependencies must point only inward, toward higher-level policies.
┌─────────────────────────────────────┐
│ External Interfaces & I/O │ ← Frameworks, Drivers, APIs
└─────────────────┬───────────────────┘
│ depends on
┌─────────────────▼───────────────────┐
│ Interface Adapters (Handlers) │ ← Controllers, Presenters
└─────────────────┬───────────────────┘
│ depends on
┌─────────────────▼───────────────────┐
│ Application Business Rules │ ← Use Cases
└─────────────────┬───────────────────┘
│ depends on
┌─────────────────▼───────────────────┐
│ Enterprise Business Rules │ ← Entities (Domain)
│ (No Dependencies) │
└─────────────────────────────────────┘Inner layers never know about outer layers.
Goca's 4 Layers
🟡 Layer 1: Domain (Entities)
Location: internal/domain/
The innermost layer containing enterprise-wide business rules.
Responsibilities
- Define business entities
- Implement core business rules
- Define domain errors
- Domain-specific validations
Example: User Entity
package domain
import (
"errors"
"strings"
"time"
)
// User represents a user entity in our system
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Validate enforces business rules
func (u *User) Validate() error {
if strings.TrimSpace(u.Name) == "" {
return ErrUserNameRequired
}
if len(u.Name) < 2 {
return ErrUserNameTooShort
}
if !u.IsValidEmail() {
return ErrInvalidEmail
}
return nil
}
// IsAdmin checks if user has admin privileges
func (u *User) IsAdmin() bool {
return u.Role == "admin"
}
// IsValidEmail validates email format (business rule)
func (u *User) IsValidEmail() bool {
return strings.Contains(u.Email, "@") && len(u.Email) > 5
}
// Domain-specific errors
var (
ErrUserNameRequired = errors.New("user name is required")
ErrUserNameTooShort = errors.New("user name must be at least 2 characters")
ErrInvalidEmail = errors.New("invalid email format")
ErrUserNotFound = errors.New("user not found")
)Domain Layer Rules
Do: Pure business logic, no external dependencies
Don't: Import HTTP, database, or framework packages
🔴 Layer 2: Use Cases (Application Logic)
Location: internal/usecase/
Contains application-specific business rules.
Responsibilities
- Define use case interfaces
- Implement application workflows
- Coordinate between repositories
- Define DTOs (Data Transfer Objects)
- Input validation
Example: User Use Case
package usecase
import (
"myproject/internal/domain"
"myproject/internal/repository"
)
// UserUseCase defines user-related operations
type UserUseCase interface {
Create(input CreateUserInput) (*CreateUserOutput, error)
GetByID(id uint) (*domain.User, error)
Update(id uint, input UpdateUserInput) (*domain.User, error)
Delete(id uint) error
List() (*ListUserOutput, error)
}
// CreateUserInput - Input DTO
type CreateUserInput struct {
Name string `json:"name" validate:"required,min=2"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=1"`
}
// CreateUserOutput - Output DTO
type CreateUserOutput struct {
User domain.User `json:"user"`
Message string `json:"message"`
}
// UpdateUserInput - Input DTO
type UpdateUserInput struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=2"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Age *int `json:"age,omitempty" validate:"omitempty,min=1"`
}
// ListUserOutput - Output DTO
type ListUserOutput struct {
Users []domain.User `json:"users"`
Total int `json:"total"`
Message string `json:"message"`
}
// userService implements UserUseCase
type userService struct {
repo repository.UserRepository // Depends on interface!
}
func NewUserService(repo repository.UserRepository) UserUseCase {
return &userService{repo: repo}
}
func (s *userService) Create(input CreateUserInput) (*CreateUserOutput, error) {
// 1. Create domain entity
user := &domain.User{
Name: input.Name,
Email: input.Email,
}
// 2. Validate business rules
if err := user.Validate(); err != nil {
return nil, err
}
// 3. Persist through repository
if err := s.repo.Save(user); err != nil {
return nil, err
}
// 4. Return DTO
return &CreateUserOutput{
User: *user,
Message: "User created successfully",
}, nil
}Use Case Layer Rules
Do: Application workflows, DTOs, coordinate repositories
Don't: HTTP/gRPC details, SQL queries, framework-specific code
🔵 Layer 3: Repository (Infrastructure)
Location: internal/repository/
Implements data access and external communication.
Responsibilities
- Define repository interfaces
- Implement repository interfaces
- Handle database operations
- Manage database connections
- Transform between DB models and domain entities
Example: GORM Repository
package repository
import (
"myproject/internal/domain"
"gorm.io/gorm"
)
// UserRepository defines data access interface
type UserRepository interface {
Save(user *domain.User) error
FindByID(id int) (*domain.User, error)
FindByEmail(email string) (*domain.User, error)
Update(user *domain.User) error
Delete(id int) error
FindAll() ([]domain.User, error)
}
type postgresUserRepository struct {
db *gorm.DB
}
// NewPostgresUserRepository creates a new repository
func NewPostgresUserRepository(db *gorm.DB) UserRepository {
return &postgresUserRepository{db: db}
}
func (r *postgresUserRepository) Save(user *domain.User) error {
result := r.db.Create(user)
return result.Error
}
func (r *postgresUserRepository) FindByID(id int) (*domain.User, error) {
user := &domain.User{}
result := r.db.First(user, id)
if result.Error != nil {
return nil, result.Error
}
return user, nil
}
func (r *postgresUserRepository) Update(user *domain.User) error {
result := r.db.Save(user)
return result.Error
}
func (r *postgresUserRepository) Delete(id int) error {
result := r.db.Delete(&domain.User{}, id)
return result.Error
}
func (r *postgresUserRepository) FindAll() ([]domain.User, error) {
var users []domain.User
result := r.db.Find(&users)
if result.Error != nil {
return nil, result.Error
}
return users, nil
}Repository Layer Rules
Do: Define and implement repository interfaces, handle persistence
Don't: Business logic, validation rules
🟢 Layer 4: Handlers (Interface Adapters)
Location: internal/handler/http/
Adapts external requests to use cases.
Responsibilities
- Handle HTTP/gRPC/CLI requests
- Parse and validate input
- Call use cases
- Format responses
- Handle HTTP-specific concerns
Example: HTTP Handler
package http
import (
"encoding/json"
"net/http"
"strconv"
"myproject/internal/usecase"
"github.com/gorilla/mux"
)
type UserHandler struct {
userUseCase usecase.UserUseCase
}
func NewUserHandler(userUseCase usecase.UserUseCase) *UserHandler {
return &UserHandler{userUseCase: userUseCase}
}
// CreateUser handles POST /users
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// 1. Parse HTTP request
var input usecase.CreateUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
// 2. Call use case
output, err := h.userUseCase.Create(input)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
// 3. Send HTTP response
respondJSON(w, http.StatusCreated, output)
}
// GetUser handles GET /users/{id}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// Parse path parameter
vars := mux.Vars(r)
id, err := strconv.ParseUint(vars["id"], 10, 32)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid user ID")
return
}
user, err := h.userUseCase.GetByID(uint(id))
if err != nil {
respondError(w, http.StatusNotFound, "User not found")
return
}
respondJSON(w, http.StatusOK, user)
}
// Helper functions
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(payload)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}Handler Layer Rules
Do: Protocol-specific concerns, request/response formatting
Don't: Business logic, direct database access
Complete Data Flow
Here's how a request flows through all layers:
1. HTTP Request
↓
2. Handler parses request → CreateUserInput DTO
↓
3. UseCase validates and applies business rules
↓
4. UseCase creates Domain Entity
↓
5. Entity validates its own business rules
↓
6. UseCase calls Repository interface
↓
7. Repository saves to database
↓
8. Repository returns Domain Entity
↓
9. UseCase transforms to CreateUserOutput DTO
↓
10. Handler formats HTTP ResponseBenefits of This Architecture
1. Testability
Test each layer in isolation:
// Test use case without HTTP or database
func TestCreateUser(t *testing.T) {
mockRepo := &MockUserRepository{}
useCase := usecase.NewUserService(mockRepo)
input := usecase.CreateUserInput{
Name: "John Doe",
Email: "john@example.com",
Age: 30,
}
output, err := useCase.Create(input)
assert.NoError(t, err)
assert.Equal(t, "John Doe", output.User.Name)
assert.Equal(t, "User created successfully", output.Message)
}2. Flexibility
Swap implementations without touching business logic:
// Switch from PostgreSQL to MongoDB
// Old: postgresRepo := repository.NewPostgresUserRepository(db)
// New: mongoRepo := repository.NewMongoUserRepository(client)
userService := usecase.NewUserService(postgresRepo) // Same interface!3. Maintainability
Changes are localized to specific layers:
- UI change? → Only handler layer
- Database change? → Only repository layer
- Business rule change? → Only domain/usecase layer
Common Mistakes to Avoid
Skip Layers
// BAD: Handler directly accessing database
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
db.Exec("INSERT INTO users...") // Skipping use case!
}Wrong Dependencies
// BAD: Domain depending on outer layer
package domain
import "net/http" // Domain shouldn't know about HTTP!Business Logic in Handlers
// BAD: Validation in handler
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
if user.Name == "" { // This belongs in domain/usecase!
return errors.New("name required")
}
}Learn More
- Project Structure - Directory organization
- Complete Tutorial - Build a real app
- Best Practices - Tips and conventions
Resources
- Clean Architecture Book by Robert C. Martin
- The Clean Architecture - Uncle Bob's blog