Go Clean Architecture Skill
Overview
Clean Architecture in Go emphasizes separation of concerns through distinct layers, with dependencies pointing inward toward the domain.
Layer Structure
Domain Layer (innermost)
Location: internal/domain/
Contains:
- Business entities (structs)
- Repository interfaces
- Domain logic and validation
- Business rules
Rules:
- NO external dependencies
- NO framework dependencies
- Pure business logic
- Defines contracts for outer layers
Example:
go
1// internal/domain/account.go
2package domain
3
4type Account struct {
5 ID string
6 Name string
7 Type AccountType
8 Balance int // cents
9}
10
11type AccountRepository interface {
12 Create(account *Account) error
13 GetByID(id string) (*Account, error)
14 Update(account *Account) error
15 Delete(id string) error
16}
17
18// Domain validation
19func (a *Account) Validate() error {
20 if a.Name == "" {
21 return ErrInvalidName
22 }
23 if !a.Type.IsValid() {
24 return ErrInvalidType
25 }
26 return nil
27}
Application Layer (middle)
Location: internal/application/
Contains:
- Business logic services
- Use case orchestration
- Service interfaces
- Cross-cutting concerns
Rules:
- Depends ONLY on domain interfaces
- NO HTTP dependencies
- NO database dependencies
- Orchestrates domain entities
Example:
go
1// internal/application/account_service.go
2package application
3
4import "internal/domain"
5
6type AccountService struct {
7 repo domain.AccountRepository // Interface, not concrete type
8}
9
10func NewAccountService(repo domain.AccountRepository) *AccountService {
11 return &AccountService{repo: repo}
12}
13
14func (s *AccountService) CreateAccount(account *domain.Account) error {
15 if err := account.Validate(); err != nil {
16 return fmt.Errorf("validation failed: %w", err)
17 }
18
19 if err := s.repo.Create(account); err != nil {
20 return fmt.Errorf("failed to create account: %w", err)
21 }
22
23 return nil
24}
Infrastructure Layer (outermost)
Location: internal/infrastructure/
Contains:
- Repository implementations
- HTTP handlers
- Database logic
- External service integrations
Rules:
- Implements domain interfaces
- Can have external dependencies
- Handlers should be thin (parse → service → respond)
- Repositories only handle persistence
Example:
go
1// internal/infrastructure/repository/account_repository.go
2package repository
3
4import (
5 "database/sql"
6 "internal/domain"
7)
8
9type AccountRepository struct {
10 db *sql.DB
11}
12
13func NewAccountRepository(db *sql.DB) *AccountRepository {
14 return &AccountRepository{db: db}
15}
16
17func (r *AccountRepository) Create(account *domain.Account) error {
18 query := `INSERT INTO accounts (id, name, type, balance) VALUES (?, ?, ?, ?)`
19 _, err := r.db.Exec(query, account.ID, account.Name, account.Type, account.Balance)
20 return err
21}
22
23// internal/infrastructure/http/handlers/account_handler.go
24package handlers
25
26type AccountHandler struct {
27 service *application.AccountService
28}
29
30func (h *AccountHandler) CreateAccount(w http.ResponseWriter, r *http.Request) {
31 // 1. Parse request
32 var req CreateAccountRequest
33 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
34 http.Error(w, "invalid request", http.StatusBadRequest)
35 return
36 }
37
38 // 2. Call service
39 account := req.ToDomain()
40 if err := h.service.CreateAccount(account); err != nil {
41 http.Error(w, err.Error(), http.StatusInternalServerError)
42 return
43 }
44
45 // 3. Return response
46 w.WriteHeader(http.StatusCreated)
47 json.NewEncoder(w).Encode(account)
48}
Dependency Injection
Wire dependencies in main.go:
go
1// cmd/server/main.go
2func main() {
3 // Infrastructure
4 db := setupDatabase()
5
6 // Repositories (concrete implementations)
7 accountRepo := repository.NewAccountRepository(db)
8
9 // Services (injected with interfaces)
10 accountService := application.NewAccountService(accountRepo)
11
12 // Handlers (injected with services)
13 accountHandler := handlers.NewAccountHandler(accountService)
14
15 // Router
16 router := setupRouter(accountHandler)
17
18 http.ListenAndServe(":8080", router)
19}
Common Patterns
Repository Pattern
go
1// Domain defines interface
2type Repository interface {
3 Create(entity *Entity) error
4 GetByID(id string) (*Entity, error)
5}
6
7// Infrastructure implements
8type SQLRepository struct {
9 db *sql.DB
10}
11
12func (r *SQLRepository) Create(entity *Entity) error {
13 // SQL implementation
14}
Service Pattern
go
1type Service struct {
2 repo domain.Repository // Depend on interface
3}
4
5func (s *Service) DoBusinessLogic(entity *domain.Entity) error {
6 // Validate
7 // Transform
8 // Call repository
9 return s.repo.Create(entity)
10}
Handler Pattern
go
1func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
2 // Parse → Service → Respond
3 req := parseRequest(r)
4 result, err := h.service.Do(req)
5 respond(w, result, err)
6}
Anti-Patterns to Avoid
❌ Domain with External Dependencies
go
1// BAD: Domain importing database
2import "database/sql"
3
4type Account struct {
5 db *sql.DB // ❌ Domain shouldn't know about database
6}
❌ Service with HTTP/Database
go
1// BAD: Service with HTTP dependency
2func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
3 // ❌ Service shouldn't handle HTTP
4}
5
6// BAD: Service with database dependency
7func (s *Service) Create(db *sql.DB, entity *Entity) error {
8 // ❌ Service should use repository interface
9}
❌ Handler with Business Logic
go
1// BAD: Complex logic in handler
2func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
3 // Parse
4 // ❌ Complex validation
5 // ❌ Calculations
6 // ❌ Business rules
7 // Direct database access
8}
9
10// GOOD: Thin handler
11func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
12 req := parse(r)
13 result := h.service.Create(req) // Service has the logic
14 respond(w, result)
15}
❌ Repository with Business Logic
go
1// BAD: Business rules in repository
2func (r *Repository) Create(account *Account) error {
3 // ❌ Business validation in repository
4 if account.Balance < 0 && account.Type != "credit" {
5 return errors.New("invalid")
6 }
7 // Should only handle persistence
8}
Testing Strategy
Domain Tests
go
1func TestAccount_Validate(t *testing.T) {
2 // Test entity validation
3 // No mocks needed
4}
Service Tests (Unit)
go
1func TestService_Create(t *testing.T) {
2 mockRepo := &MockRepository{} // Mock interface
3 service := NewService(mockRepo)
4 // Test business logic
5}
Repository Tests (Integration)
go
1func TestRepository_Create(t *testing.T) {
2 db := setupTestDB() // Real database
3 repo := NewRepository(db)
4 // Test persistence
5}
Handler Tests (E2E)
go
1func TestHandler_Create(t *testing.T) {
2 mockService := &MockService{}
3 handler := NewHandler(mockService)
4 req := httptest.NewRequest("POST", "/", body)
5 w := httptest.NewRecorder()
6 handler.Create(w, req)
7 // Test HTTP layer
8}
Benefits
✅ Testability: Easy to mock dependencies
✅ Maintainability: Clear separation of concerns
✅ Flexibility: Easy to swap implementations
✅ Independence: Domain logic independent of frameworks
✅ Scalability: Easy to add features
When to Apply
- Multi-layer applications
- Complex business logic
- Long-lived projects
- Team projects requiring clear boundaries
- Applications that may change databases/frameworks
Quick Checklist