Go Architecture Patterns Series: ← Layered Architecture | Series Overview | Next: Hexagonal Architecture →
What is Clean Architecture?
Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is an architectural pattern that emphasizes separation of concerns and independence from frameworks, databases, and external agencies. It organizes code in concentric circles with dependencies pointing inward.
Key Principles:
- Independence of Frameworks: Architecture doesn’t depend on frameworks
- Testability: Business rules can be tested without UI, database, or external elements
- Independence of UI: UI can change without changing business rules
- Independence of Database: Business rules don’t know about the database
- Independence of External Agency: Business rules don’t know about the outside world
- Dependency Rule: Dependencies only point inward toward higher-level policies
Architecture Overview
Clean Architecture Circles
Rules & Models] end subgraph "Layer 2: Use Cases" U[Application
Business Rules] end subgraph "Layer 3: Interface Adapters" I[Controllers
Presenters
Gateways] end subgraph "Layer 4: Frameworks & Drivers" F[Web
DB
UI] end F -.->|depends on| I I -.->|depends on| U U -.->|depends on| E style E fill:#FFD700 style U fill:#87CEEB style I fill:#90EE90 style F fill:#FFB6C1
Dependency Flow
Implementation] Controller[HTTP Controller] Gateway[API Gateway] end subgraph "Use Cases" direction LR UC1[Create User
Use Case] UC2[Get User
Use Case] end subgraph "Entities (Core)" direction LR Entity[User Entity] Rules[Business Rules] end DB --> Repo Web --> Controller API --> Gateway Repo -.->|implements| UC1 Controller -.->|calls| UC1 Gateway -.->|implements| UC2 UC1 -.->|uses| Entity UC2 -.->|uses| Entity style Entity fill:#FFD700,stroke:#FF8C00,stroke-width:3px style UC1 fill:#87CEEB style Repo fill:#90EE90 style DB fill:#FFB6C1
Real-World Use Cases
- Enterprise Applications: Complex business logic that needs isolation
- Long-lived Systems: Applications that need to evolve over time
- Multi-platform Applications: Same core logic, different interfaces
- Testing-Critical Systems: Financial, healthcare, or mission-critical apps
- API-first Applications: Where business logic is reused across interfaces
- Microservices: Each service following clean architecture principles
Project Structure
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── entity/ # Layer 1: Entities
│ │ ├── user.go
│ │ └── errors.go
│ ├── usecase/ # Layer 2: Use Cases
│ │ ├── user_usecase.go
│ │ ├── interfaces.go # Repository & Presenter interfaces
│ │ └── user_interactor.go
│ ├── adapter/ # Layer 3: Interface Adapters
│ │ ├── repository/
│ │ │ └── user_repository.go
│ │ ├── presenter/
│ │ │ └── user_presenter.go
│ │ └── controller/
│ │ └── user_controller.go
│ └── infrastructure/ # Layer 4: Frameworks & Drivers
│ ├── database/
│ │ └── postgres.go
│ ├── router/
│ │ └── router.go
│ └── config/
│ └── config.go
└── go.mod
Layer 1: Entities (Core Business Rules)
package entity
import (
"errors"
"regexp"
"time"
)
// User represents the core user entity with business rules
type User struct {
ID string
Email string
Name string
Age int
Status UserStatus
CreatedAt time.Time
UpdatedAt time.Time
}
// UserStatus represents user account status
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusSuspended UserStatus = "suspended"
)
// Business rule validation errors
var (
ErrInvalidEmail = errors.New("invalid email format")
ErrInvalidName = errors.New("name must not be empty")
ErrInvalidAge = errors.New("age must be between 0 and 150")
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
ErrUnauthorized = errors.New("unauthorized action")
)
// NewUser creates a new user with business rule validation
func NewUser(email, name string, age int) (*User, error) {
user := &User{
Email: email,
Name: name,
Age: age,
Status: UserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := user.Validate(); err != nil {
return nil, err
}
return user, nil
}
// Validate validates the user entity according to business rules
func (u *User) Validate() error {
// Business Rule: Email must be valid format
if !isValidEmail(u.Email) {
return ErrInvalidEmail
}
// Business Rule: Name must not be empty
if u.Name == "" {
return ErrInvalidName
}
// Business Rule: Age must be realistic
if u.Age < 0 || u.Age > 150 {
return ErrInvalidAge
}
return nil
}
// UpdateEmail updates user email with validation
func (u *User) UpdateEmail(email string) error {
if !isValidEmail(email) {
return ErrInvalidEmail
}
u.Email = email
u.UpdatedAt = time.Now()
return nil
}
// UpdateName updates user name with validation
func (u *User) UpdateName(name string) error {
if name == "" {
return ErrInvalidName
}
u.Name = name
u.UpdatedAt = time.Now()
return nil
}
// Activate activates the user account
func (u *User) Activate() {
u.Status = UserStatusActive
u.UpdatedAt = time.Now()
}
// Deactivate deactivates the user account
func (u *User) Deactivate() {
u.Status = UserStatusInactive
u.UpdatedAt = time.Now()
}
// Suspend suspends the user account
func (u *User) Suspend() {
u.Status = UserStatusSuspended
u.UpdatedAt = time.Now()
}
// IsActive checks if user is active
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}
// CanPerformAction checks if user can perform actions (business rule)
func (u *User) CanPerformAction() bool {
return u.Status == UserStatusActive
}
// isValidEmail validates email format
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// Product entity with business rules
type Product struct {
ID string
Name string
Description string
Price Money
Stock int
CreatedAt time.Time
UpdatedAt time.Time
}
// Money represents monetary value (value object)
type Money struct {
Amount int64 // in cents
Currency string
}
// NewMoney creates a new money value object
func NewMoney(amount int64, currency string) Money {
return Money{
Amount: amount,
Currency: currency,
}
}
// Add adds two money values
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("currency mismatch")
}
return Money{
Amount: m.Amount + other.Amount,
Currency: m.Currency,
}, nil
}
// Multiply multiplies money by a factor
func (m Money) Multiply(factor int) Money {
return Money{
Amount: m.Amount * int64(factor),
Currency: m.Currency,
}
}
Layer 2: Use Cases (Application Business Rules)
Use Case Interfaces
package usecase
import (
"context"
"myapp/internal/entity"
)
// UserRepository defines the interface for user data access
// This interface is defined in the use case layer but implemented in outer layers
type UserRepository interface {
Create(ctx context.Context, user *entity.User) error
GetByID(ctx context.Context, id string) (*entity.User, error)
GetByEmail(ctx context.Context, email string) (*entity.User, error)
Update(ctx context.Context, user *entity.User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, offset, limit int) ([]*entity.User, error)
}
// UserPresenter defines the interface for presenting user data
type UserPresenter interface {
PresentUser(user *entity.User) interface{}
PresentUsers(users []*entity.User) interface{}
PresentError(err error) interface{}
}
// EmailService defines the interface for email operations
type EmailService interface {
SendWelcomeEmail(ctx context.Context, user *entity.User) error
SendPasswordResetEmail(ctx context.Context, user *entity.User, token string) error
}
// IDGenerator defines the interface for generating IDs
type IDGenerator interface {
Generate() string
}
Use Case Implementation
package usecase
import (
"context"
"fmt"
"myapp/internal/entity"
)
// CreateUserInput represents input for creating a user
type CreateUserInput struct {
Email string
Name string
Age int
}
// UpdateUserInput represents input for updating a user
type UpdateUserInput struct {
ID string
Email string
Name string
Age int
}
// UserInteractor implements user use cases
type UserInteractor struct {
repo UserRepository
emailService EmailService
idGen IDGenerator
}
// NewUserInteractor creates a new user interactor
func NewUserInteractor(
repo UserRepository,
emailService EmailService,
idGen IDGenerator,
) *UserInteractor {
return &UserInteractor{
repo: repo,
emailService: emailService,
idGen: idGen,
}
}
// CreateUser creates a new user (use case)
func (i *UserInteractor) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) {
// Use case logic: Check if user already exists
existingUser, err := i.repo.GetByEmail(ctx, input.Email)
if err == nil && existingUser != nil {
return nil, entity.ErrUserAlreadyExists
}
// Use case logic: Create new user entity
user, err := entity.NewUser(input.Email, input.Name, input.Age)
if err != nil {
return nil, fmt.Errorf("failed to create user entity: %w", err)
}
// Use case logic: Generate ID
user.ID = i.idGen.Generate()
// Use case logic: Save user
if err := i.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("failed to save user: %w", err)
}
// Use case logic: Send welcome email (async in real system)
if err := i.emailService.SendWelcomeEmail(ctx, user); err != nil {
// Log error but don't fail the use case
fmt.Printf("failed to send welcome email: %v\n", err)
}
return user, nil
}
// GetUser retrieves a user by ID (use case)
func (i *UserInteractor) GetUser(ctx context.Context, id string) (*entity.User, error) {
user, err := i.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
return nil, entity.ErrUserNotFound
}
return user, nil
}
// UpdateUser updates a user (use case)
func (i *UserInteractor) UpdateUser(ctx context.Context, input UpdateUserInput) (*entity.User, error) {
// Use case logic: Get existing user
user, err := i.repo.GetByID(ctx, input.ID)
if err != nil {
return nil, entity.ErrUserNotFound
}
// Use case logic: Check if email is changing and if it already exists
if user.Email != input.Email {
existingUser, err := i.repo.GetByEmail(ctx, input.Email)
if err == nil && existingUser != nil {
return nil, entity.ErrUserAlreadyExists
}
}
// Use case logic: Update user fields
if err := user.UpdateEmail(input.Email); err != nil {
return nil, err
}
if err := user.UpdateName(input.Name); err != nil {
return nil, err
}
user.Age = input.Age
if err := user.Validate(); err != nil {
return nil, err
}
// Use case logic: Save updated user
if err := i.repo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return user, nil
}
// DeleteUser deletes a user (use case)
func (i *UserInteractor) DeleteUser(ctx context.Context, id string) error {
// Use case logic: Verify user exists
user, err := i.repo.GetByID(ctx, id)
if err != nil {
return entity.ErrUserNotFound
}
// Use case logic: Check if user can be deleted (business rule)
if !user.CanPerformAction() {
return entity.ErrUnauthorized
}
// Use case logic: Delete user
if err := i.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
// ListUsers lists users with pagination (use case)
func (i *UserInteractor) ListUsers(ctx context.Context, page, pageSize int) ([]*entity.User, error) {
// Use case logic: Validate pagination
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
offset := (page - 1) * pageSize
// Use case logic: Get users
users, err := i.repo.List(ctx, offset, pageSize)
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
return users, nil
}
// ActivateUser activates a user account (use case)
func (i *UserInteractor) ActivateUser(ctx context.Context, id string) error {
user, err := i.repo.GetByID(ctx, id)
if err != nil {
return entity.ErrUserNotFound
}
user.Activate()
if err := i.repo.Update(ctx, user); err != nil {
return fmt.Errorf("failed to activate user: %w", err)
}
return nil
}
Layer 3: Interface Adapters
Repository Implementation
package repository
import (
"context"
"database/sql"
"fmt"
"time"
"myapp/internal/entity"
)
// PostgresUserRepository implements UserRepository interface
type PostgresUserRepository struct {
db *sql.DB
}
// NewPostgresUserRepository creates a new Postgres user repository
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
// Create creates a new user in the database
func (r *PostgresUserRepository) Create(ctx context.Context, user *entity.User) error {
query := `
INSERT INTO users (id, email, name, age, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
_, err := r.db.ExecContext(
ctx, query,
user.ID,
user.Email,
user.Name,
user.Age,
user.Status,
user.CreatedAt,
user.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
// GetByID retrieves a user by ID
func (r *PostgresUserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
query := `
SELECT id, email, name, age, status, created_at, updated_at
FROM users
WHERE id = $1
`
var user entity.User
var status string
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Age,
&status,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
user.Status = entity.UserStatus(status)
return &user, nil
}
// GetByEmail retrieves a user by email
func (r *PostgresUserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
query := `
SELECT id, email, name, age, status, created_at, updated_at
FROM users
WHERE email = $1
`
var user entity.User
var status string
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Age,
&status,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
user.Status = entity.UserStatus(status)
return &user, nil
}
// Update updates a user
func (r *PostgresUserRepository) Update(ctx context.Context, user *entity.User) error {
query := `
UPDATE users
SET email = $1, name = $2, age = $3, status = $4, updated_at = $5
WHERE id = $6
`
result, err := r.db.ExecContext(
ctx, query,
user.Email,
user.Name,
user.Age,
user.Status,
time.Now(),
user.ID,
)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrUserNotFound
}
return nil
}
// Delete deletes a user
func (r *PostgresUserRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrUserNotFound
}
return nil
}
// List retrieves users with pagination
func (r *PostgresUserRepository) List(ctx context.Context, offset, limit int) ([]*entity.User, error) {
query := `
SELECT id, email, name, age, status, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
defer rows.Close()
var users []*entity.User
for rows.Next() {
var user entity.User
var status string
err := rows.Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Age,
&status,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
user.Status = entity.UserStatus(status)
users = append(users, &user)
}
return users, nil
}
Controller Implementation
package controller
import (
"encoding/json"
"net/http"
"strconv"
"myapp/internal/entity"
"myapp/internal/usecase"
)
// UserController handles HTTP requests for users
type UserController struct {
interactor *usecase.UserInteractor
presenter usecase.UserPresenter
}
// NewUserController creates a new user controller
func NewUserController(
interactor *usecase.UserInteractor,
presenter usecase.UserPresenter,
) *UserController {
return &UserController{
interactor: interactor,
presenter: presenter,
}
}
// CreateUserRequest represents the HTTP request for creating a user
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Age int `json:"age"`
}
// CreateUser handles POST /users
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
c.respondError(w, http.StatusBadRequest, err)
return
}
input := usecase.CreateUserInput{
Email: req.Email,
Name: req.Name,
Age: req.Age,
}
user, err := c.interactor.CreateUser(r.Context(), input)
if err != nil {
switch err {
case entity.ErrInvalidEmail, entity.ErrInvalidName, entity.ErrInvalidAge:
c.respondError(w, http.StatusBadRequest, err)
case entity.ErrUserAlreadyExists:
c.respondError(w, http.StatusConflict, err)
default:
c.respondError(w, http.StatusInternalServerError, err)
}
return
}
c.respond(w, http.StatusCreated, c.presenter.PresentUser(user))
}
// GetUser handles GET /users/:id
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
user, err := c.interactor.GetUser(r.Context(), id)
if err != nil {
if err == entity.ErrUserNotFound {
c.respondError(w, http.StatusNotFound, err)
} else {
c.respondError(w, http.StatusInternalServerError, err)
}
return
}
c.respond(w, http.StatusOK, c.presenter.PresentUser(user))
}
// UpdateUser handles PUT /users/:id
func (c *UserController) UpdateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
c.respondError(w, http.StatusBadRequest, err)
return
}
input := usecase.UpdateUserInput{
ID: id,
Email: req.Email,
Name: req.Name,
Age: req.Age,
}
user, err := c.interactor.UpdateUser(r.Context(), input)
if err != nil {
switch err {
case entity.ErrUserNotFound:
c.respondError(w, http.StatusNotFound, err)
case entity.ErrInvalidEmail, entity.ErrInvalidName, entity.ErrInvalidAge:
c.respondError(w, http.StatusBadRequest, err)
case entity.ErrUserAlreadyExists:
c.respondError(w, http.StatusConflict, err)
default:
c.respondError(w, http.StatusInternalServerError, err)
}
return
}
c.respond(w, http.StatusOK, c.presenter.PresentUser(user))
}
// DeleteUser handles DELETE /users/:id
func (c *UserController) DeleteUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := c.interactor.DeleteUser(r.Context(), id); err != nil {
if err == entity.ErrUserNotFound {
c.respondError(w, http.StatusNotFound, err)
} else {
c.respondError(w, http.StatusInternalServerError, err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
// ListUsers handles GET /users
func (c *UserController) ListUsers(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
users, err := c.interactor.ListUsers(r.Context(), page, pageSize)
if err != nil {
c.respondError(w, http.StatusInternalServerError, err)
return
}
c.respond(w, http.StatusOK, c.presenter.PresentUsers(users))
}
// Helper methods
func (c *UserController) respond(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (c *UserController) respondError(w http.ResponseWriter, status int, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(c.presenter.PresentError(err))
}
Presenter Implementation
package presenter
import (
"myapp/internal/entity"
)
// UserJSONPresenter presents users as JSON
type UserJSONPresenter struct{}
// NewUserJSONPresenter creates a new user JSON presenter
func NewUserJSONPresenter() *UserJSONPresenter {
return &UserJSONPresenter{}
}
// UserResponse represents the JSON response for a user
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Age int `json:"age"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// PresentUser presents a single user
func (p *UserJSONPresenter) PresentUser(user *entity.User) interface{} {
return UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Age: user.Age,
Status: string(user.Status),
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// PresentUsers presents multiple users
func (p *UserJSONPresenter) PresentUsers(users []*entity.User) interface{} {
responses := make([]UserResponse, len(users))
for i, user := range users {
responses[i] = UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Age: user.Age,
Status: string(user.Status),
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
return map[string]interface{}{
"users": responses,
"count": len(responses),
}
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Code string `json:"code,omitempty"`
}
// PresentError presents an error
func (p *UserJSONPresenter) PresentError(err error) interface{} {
return ErrorResponse{
Error: err.Error(),
}
}
Layer 4: Main Application (Dependency Injection)
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/lib/pq"
"github.com/google/uuid"
"myapp/internal/adapter/controller"
"myapp/internal/adapter/presenter"
"myapp/internal/adapter/repository"
"myapp/internal/usecase"
)
// UUIDGenerator implements IDGenerator
type UUIDGenerator struct{}
func (g *UUIDGenerator) Generate() string {
return uuid.New().String()
}
// MockEmailService implements EmailService
type MockEmailService struct{}
func (s *MockEmailService) SendWelcomeEmail(ctx context.Context, user *entity.User) error {
log.Printf("Sending welcome email to %s", user.Email)
return nil
}
func (s *MockEmailService) SendPasswordResetEmail(ctx context.Context, user *entity.User, token string) error {
log.Printf("Sending password reset email to %s with token %s", user.Email, token)
return nil
}
func main() {
// Initialize database
db, err := sql.Open("postgres", "postgres://user:pass@localhost/cleanarch?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Layer 4: Initialize adapters (implementations)
userRepo := repository.NewPostgresUserRepository(db)
emailService := &MockEmailService{}
idGen := &UUIDGenerator{}
userPresenter := presenter.NewUserJSONPresenter()
// Layer 2: Initialize use cases (with dependency injection)
userInteractor := usecase.NewUserInteractor(userRepo, emailService, idGen)
// Layer 3: Initialize controllers
userController := controller.NewUserController(userInteractor, userPresenter)
// Setup routes
mux := http.NewServeMux()
mux.HandleFunc("POST /users", userController.CreateUser)
mux.HandleFunc("GET /users/{id}", userController.GetUser)
mux.HandleFunc("PUT /users/{id}", userController.UpdateUser)
mux.HandleFunc("DELETE /users/{id}", userController.DeleteUser)
mux.HandleFunc("GET /users", userController.ListUsers)
// Start server
log.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
Testing in Clean Architecture
package usecase_test
import (
"context"
"testing"
"myapp/internal/entity"
"myapp/internal/usecase"
)
// Mock implementations
type MockUserRepository struct {
users map[string]*entity.User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[string]*entity.User),
}
}
func (m *MockUserRepository) Create(ctx context.Context, user *entity.User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
user, exists := m.users[id]
if !exists {
return nil, entity.ErrUserNotFound
}
return user, nil
}
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*entity.User, error) {
for _, user := range m.users {
if user.Email == email {
return user, nil
}
}
return nil, nil
}
type MockEmailService struct {
sentEmails []string
}
func (m *MockEmailService) SendWelcomeEmail(ctx context.Context, user *entity.User) error {
m.sentEmails = append(m.sentEmails, user.Email)
return nil
}
type MockIDGenerator struct {
nextID int
}
func (m *MockIDGenerator) Generate() string {
m.nextID++
return fmt.Sprintf("user-%d", m.nextID)
}
func TestCreateUser(t *testing.T) {
// Arrange
repo := NewMockUserRepository()
emailService := &MockEmailService{}
idGen := &MockIDGenerator{}
interactor := usecase.NewUserInteractor(repo, emailService, idGen)
input := usecase.CreateUserInput{
Email: "[email protected]",
Name: "Test User",
Age: 25,
}
// Act
user, err := interactor.CreateUser(context.Background(), input)
// Assert
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if user.Email != input.Email {
t.Errorf("expected email %s, got %s", input.Email, user.Email)
}
if len(emailService.sentEmails) != 1 {
t.Errorf("expected 1 welcome email, got %d", len(emailService.sentEmails))
}
}
Best Practices
- Dependency Rule: Always point dependencies inward
- Interface Segregation: Define minimal interfaces in use case layer
- Dependency Injection: Inject all dependencies explicitly
- Entity Purity: Keep entities free from framework dependencies
- Use Case Focus: Each use case should have a single responsibility
- Test Independence: Test each layer independently
- Avoid Anemic Models: Put business logic in entities
Common Pitfalls
- Breaking Dependency Rule: Outer layers should not be imported by inner layers
- Leaking Infrastructure: Database or framework details leaking into entities
- Fat Use Cases: Use cases doing too much or too little
- Ignoring Presenters: Directly returning entities from controllers
- Over-engineering: Applying Clean Architecture to simple CRUD apps
- Missing Boundaries: Not clearly defining layer boundaries
When to Use Clean Architecture
Use When:
- Building complex business applications
- Long-term maintainability is critical
- Multiple interfaces (web, CLI, mobile) share the same logic
- Testing is a high priority
- Team size is medium to large
Avoid When:
- Building simple CRUD applications
- Rapid prototyping is needed
- Team is unfamiliar with the pattern
- Project timeline is very short
- Application complexity doesn’t justify the overhead
Advantages vs Disadvantages
Advantages:
- Highly testable business logic
- Framework independence
- Database independence
- Clear separation of concerns
- Easy to swap implementations
- Domain-centric design
Disadvantages:
- Steeper learning curve
- More boilerplate code
- Can be over-engineering for simple apps
- Requires discipline to maintain
- More files and abstractions
Clean Architecture provides a robust foundation for building maintainable, testable applications where business logic is king and external dependencies are just implementation details.
Go Architecture Patterns Series: ← Layered Architecture | Series Overview | Next: Hexagonal Architecture →