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

265 lines
7.4 KiB
Markdown

# 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**:
```go
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**:
```go
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**:
```go
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:
```go
// 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
```go
// ✅ 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 |
```go
// 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)
// ...
}
```