| 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:
package domain
type Account struct {
ID string
Name string
Type AccountType
Balance int
}
type AccountRepository interface {
Create(account *Account) error
GetByID(id string) (*Account, error)
Update(account *Account) error
Delete(id string) error
}
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:
package application
import "internal/domain"
type AccountService struct {
repo domain.AccountRepository
}
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:
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
}
package handlers
type AccountHandler struct {
service *application.AccountService
}
func (h *AccountHandler) CreateAccount(w http.ResponseWriter, r *http.Request) {
var req CreateAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
account := req.ToDomain()
if err := h.service.CreateAccount(account); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(account)
}
Dependency Injection
Wire dependencies in main.go:
func main() {
db := setupDatabase()
accountRepo := repository.NewAccountRepository(db)
accountService := application.NewAccountService(accountRepo)
accountHandler := handlers.NewAccountHandler(accountService)
router := setupRouter(accountHandler)
http.ListenAndServe(":8080", router)
}
Common Patterns
Repository Pattern
type Repository interface {
Create(entity *Entity) error
GetByID(id string) (*Entity, error)
}
type SQLRepository struct {
db *sql.DB
}
func (r *SQLRepository) Create(entity *Entity) error {
}
Service Pattern
type Service struct {
repo domain.Repository
}
func (s *Service) DoBusinessLogic(entity *domain.Entity) error {
return s.repo.Create(entity)
}
Handler Pattern
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
req := parseRequest(r)
result, err := h.service.Do(req)
respond(w, result, err)
}
Anti-Patterns to Avoid
❌ Domain with External Dependencies
import "database/sql"
type Account struct {
db *sql.DB
}
❌ Service with HTTP/Database
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) Create(db *sql.DB, entity *Entity) error {
}
❌ Handler with Business Logic
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
req := parse(r)
result := h.service.Create(req)
respond(w, result)
}
❌ Repository with Business Logic
func (r *Repository) Create(account *Account) error {
if account.Balance < 0 && account.Type != "credit" {
return errors.New("invalid")
}
}
Testing Strategy
Domain Tests
func TestAccount_Validate(t *testing.T) {
}
Service Tests (Unit)
func TestService_Create(t *testing.T) {
mockRepo := &MockRepository{}
service := NewService(mockRepo)
}
Repository Tests (Integration)
func TestRepository_Create(t *testing.T) {
db := setupTestDB()
repo := NewRepository(db)
}
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)
}
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