1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:49:57 +01:00
effigenix/bin/.claude/skills/ddd-model/rules/clean-arch.md
2026-02-18 23:25:12 +01:00

7.4 KiB

Clean Architecture for Go DDD Projects

Directory Structure

project-root/
├── cmd/
│   └── api/
│       └── main.go              # Application entry point
├── internal/
│   ├── domain/                  # CORE - No external dependencies
│   │   └── <bounded-context>/
│   │       ├── aggregate.go     # Aggregate roots
│   │       ├── entity.go        # Child entities
│   │       ├── value_objects.go # Value objects
│   │       ├── repository.go    # Repository INTERFACES
│   │       ├── service.go       # Domain services
│   │       ├── events.go        # Domain events
│   │       └── errors.go        # Domain-specific errors
│   │
│   ├── application/             # USE CASES - Depends on Domain
│   │   └── <bounded-context>/
│   │       ├── service.go       # Application services
│   │       ├── commands.go      # Command handlers (CQRS)
│   │       ├── queries.go       # Query handlers (CQRS)
│   │       └── dto.go           # Data Transfer Objects
│   │
│   └── infrastructure/          # ADAPTERS - Depends on Domain & App
│       ├── persistence/
│       │   └── <bounded-context>/
│       │       ├── postgres_repository.go
│       │       ├── memory_repository.go
│       │       └── models.go    # DB models (separate from domain)
│       ├── messaging/
│       │   └── kafka_publisher.go
│       └── http/
│           ├── handlers/
│           │   └── <bounded-context>_handler.go
│           ├── middleware/
│           └── router.go
├── pkg/                         # Shared utilities (if needed)
│   └── errors/
│       └── errors.go
└── go.mod

Layer Responsibilities

Domain Layer (internal/domain/)

Purpose: Core business logic, independent of all frameworks and infrastructure.

Contains:

  • Aggregates, Entities, Value Objects
  • Repository interfaces (not implementations!)
  • Domain Services
  • Domain Events
  • Domain Errors

Rules:

  • NO imports from application/ or infrastructure/
  • NO database packages, HTTP packages, or framework code
  • NO struct tags (json:, gorm:, etc.)
  • Pure Go, pure business logic

Example:

package accounts

// Domain layer - NO external imports

type Account struct {
    id      AccountID
    balance Money
    status  Status
}

// Repository interface - implementation is in infrastructure
type AccountRepository interface {
    Save(ctx context.Context, account *Account) error
    FindByID(ctx context.Context, id AccountID) (*Account, error)
}

Application Layer (internal/application/)

Purpose: Orchestrate use cases, coordinate domain objects.

Contains:

  • Application Services (Use Cases)
  • Command/Query Handlers (CQRS)
  • DTOs for input/output
  • Transaction management

Rules:

  • Imports from domain/ only
  • NO direct database access
  • NO HTTP/transport concerns
  • Coordinates domain objects, doesn't contain business logic

Example:

package accounts

import (
    "context"
    "myapp/internal/domain/accounts"
)

type TransferService struct {
    accountRepo accounts.AccountRepository
    txManager   TransactionManager
}

func (s *TransferService) Transfer(ctx context.Context, cmd TransferCommand) error {
    return s.txManager.Execute(ctx, func(ctx context.Context) error {
        from, err := s.accountRepo.FindByID(ctx, cmd.FromAccountID)
        if err != nil {
            return err
        }
        to, err := s.accountRepo.FindByID(ctx, cmd.ToAccountID)
        if err != nil {
            return err
        }

        // Domain logic in domain objects
        if err := from.Withdraw(cmd.Amount); err != nil {
            return err
        }
        if err := to.Deposit(cmd.Amount); err != nil {
            return err
        }

        // Coordinate persistence
        if err := s.accountRepo.Save(ctx, from); err != nil {
            return err
        }
        return s.accountRepo.Save(ctx, to)
    })
}

Infrastructure Layer (internal/infrastructure/)

Purpose: Implement interfaces defined in domain, connect to external systems.

Contains:

  • Repository implementations (Postgres, Redis, etc.)
  • Message queue publishers/consumers
  • External API clients
  • HTTP handlers
  • Database models (separate from domain entities)

Rules:

  • Implements interfaces from domain/
  • Can import from domain/ and application/
  • Contains all framework-specific code
  • Handles mapping between domain and persistence models

Example:

package persistence

import (
    "context"
    "database/sql"
    "myapp/internal/domain/accounts"
)

// Compile-time check
var _ accounts.AccountRepository = (*PostgresAccountRepository)(nil)

type PostgresAccountRepository struct {
    db *sql.DB
}

func (r *PostgresAccountRepository) Save(ctx context.Context, account *accounts.Account) error {
    // Map domain to DB model
    model := toDBModel(account)
    // Persist
    _, err := r.db.ExecContext(ctx, "INSERT INTO accounts ...", ...)
    return err
}

func (r *PostgresAccountRepository) FindByID(ctx context.Context, id accounts.AccountID) (*accounts.Account, error) {
    var model accountDBModel
    err := r.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id.String()).Scan(...)
    if err != nil {
        return nil, err
    }
    // Map DB model to domain
    return toDomain(model), nil
}

Dependency Injection

Wire dependencies at application startup:

// cmd/api/main.go
func main() {
    // Infrastructure
    db := connectDB()
    accountRepo := persistence.NewPostgresAccountRepository(db)

    // Application
    transferService := application.NewTransferService(accountRepo)

    // HTTP
    handler := http.NewAccountHandler(transferService)

    // Start server
    router := setupRouter(handler)
    http.ListenAndServe(":8080", router)
}

Package Naming Conventions

Layer Package Name Example
Domain Bounded context name accounts, transfers, loyalty
Application Same as domain accounts (in application/accounts/)
Infrastructure Descriptive persistence, messaging, http

Import Rules

// ✅ ALLOWED
domain/accounts -> (nothing external)
application/accounts -> domain/accounts
infrastructure/persistence -> domain/accounts, application/accounts

// ❌ NOT ALLOWED
domain/accounts -> application/accounts      // Domain can't know application
domain/accounts -> infrastructure/           // Domain can't know infra
application/accounts -> infrastructure/      // App can't know infra

Testing Strategy

Layer Test Type Mocking
Domain Unit tests No mocks needed
Application Unit tests Mock repositories
Infrastructure Integration tests Real DB (testcontainers)
E2E API tests Full stack
// Domain test - no mocks
func TestAccount_Withdraw(t *testing.T) {
    account := accounts.NewAccount(id, accounts.MustMoney(100))
    err := account.Withdraw(accounts.MustMoney(50))
    assert.NoError(t, err)
    assert.Equal(t, accounts.MustMoney(50), account.Balance())
}

// Application test - mock repository
func TestTransferService(t *testing.T) {
    repo := &MockAccountRepository{}
    service := NewTransferService(repo)
    // ...
}