| name | go-clean-architecture |
| description | Expert knowledge in Go clean architecture patterns and best practices |
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:
// internal/domain/account.go
package domain
type Account struct {
ID string
Name string
Type AccountType
Balance int // cents
}
type AccountRepository interface {
Create(account *Account) error
GetByID(id string) (*Account, error)
Update(account *Account) error
Delete(id string) error
}
// Domain validation
func (a *Account) Validate() error {
if a.Name == "" {
return ErrInvalidName
}
if !a.Type.IsValid() {
return ErrInvalidType
}
return nil
}
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:
// internal/application/account_service.go
package application
import "internal/domain"
type AccountService struct {
repo domain.AccountRepository // Interface, not concrete type
}
func NewAccountService(repo domain.AccountRepository) *AccountService {
return &AccountService{repo: repo}
}
func (s *AccountService) CreateAccount(account *domain.Account) error {
if err := account.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
if err := s.repo.Create(account); err != nil {
return fmt.Errorf("failed to create account: %w", err)
}
return nil
}
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:
// internal/infrastructure/repository/account_repository.go
package repository
import (
"database/sql"
"internal/domain"
)
type AccountRepository struct {
db *sql.DB
}
func NewAccountRepository(db *sql.DB) *AccountRepository {
return &AccountRepository{db: db}
}
func (r *AccountRepository) Create(account *domain.Account) error {
query := `INSERT INTO accounts (id, name, type, balance) VALUES (?, ?, ?, ?)`
_, err := r.db.Exec(query, account.ID, account.Name, account.Type, account.Balance)
return err
}
// internal/infrastructure/http/handlers/account_handler.go
package handlers
type AccountHandler struct {
service *application.AccountService
}
func (h *AccountHandler) CreateAccount(w http.ResponseWriter, r *http.Request) {
// 1. Parse request
var req CreateAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
// 2. Call service
account := req.ToDomain()
if err := h.service.CreateAccount(account); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 3. Return response
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(account)
}
Dependency Injection
Wire dependencies in main.go:
// cmd/server/main.go
func main() {
// Infrastructure
db := setupDatabase()
// Repositories (concrete implementations)
accountRepo := repository.NewAccountRepository(db)
// Services (injected with interfaces)
accountService := application.NewAccountService(accountRepo)
// Handlers (injected with services)
accountHandler := handlers.NewAccountHandler(accountService)
// Router
router := setupRouter(accountHandler)
http.ListenAndServe(":8080", router)
}
Common Patterns
Repository Pattern
// Domain defines interface
type Repository interface {
Create(entity *Entity) error
GetByID(id string) (*Entity, error)
}
// Infrastructure implements
type SQLRepository struct {
db *sql.DB
}
func (r *SQLRepository) Create(entity *Entity) error {
// SQL implementation
}
Service Pattern
type Service struct {
repo domain.Repository // Depend on interface
}
func (s *Service) DoBusinessLogic(entity *domain.Entity) error {
// Validate
// Transform
// Call repository
return s.repo.Create(entity)
}
Handler Pattern
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
// Parse → Service → Respond
req := parseRequest(r)
result, err := h.service.Do(req)
respond(w, result, err)
}
Anti-Patterns to Avoid
❌ Domain with External Dependencies
// BAD: Domain importing database
import "database/sql"
type Account struct {
db *sql.DB // ❌ Domain shouldn't know about database
}
❌ Service with HTTP/Database
// BAD: Service with HTTP dependency
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
// ❌ Service shouldn't handle HTTP
}
// BAD: Service with database dependency
func (s *Service) Create(db *sql.DB, entity *Entity) error {
// ❌ Service should use repository interface
}
❌ Handler with Business Logic
// BAD: Complex logic in handler
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
// Parse
// ❌ Complex validation
// ❌ Calculations
// ❌ Business rules
// Direct database access
}
// GOOD: Thin handler
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
req := parse(r)
result := h.service.Create(req) // Service has the logic
respond(w, result)
}
❌ Repository with Business Logic
// BAD: Business rules in repository
func (r *Repository) Create(account *Account) error {
// ❌ Business validation in repository
if account.Balance < 0 && account.Type != "credit" {
return errors.New("invalid")
}
// Should only handle persistence
}
Testing Strategy
Domain Tests
func TestAccount_Validate(t *testing.T) {
// Test entity validation
// No mocks needed
}
Service Tests (Unit)
func TestService_Create(t *testing.T) {
mockRepo := &MockRepository{} // Mock interface
service := NewService(mockRepo)
// Test business logic
}
Repository Tests (Integration)
func TestRepository_Create(t *testing.T) {
db := setupTestDB() // Real database
repo := NewRepository(db)
// Test persistence
}
Handler Tests (E2E)
func TestHandler_Create(t *testing.T) {
mockService := &MockService{}
handler := NewHandler(mockService)
req := httptest.NewRequest("POST", "/", body)
w := httptest.NewRecorder()
handler.Create(w, req)
// Test HTTP layer
}
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
- Domain has no external dependencies
- Application uses interfaces, not concrete types
- Handlers are thin (parse → service → respond)
- Repositories only handle persistence
- Dependencies point inward
- Business logic in services, not handlers
- Each layer has clear responsibility