mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:49:36 +01:00
docs: and skills
This commit is contained in:
parent
e4f0665086
commit
ccd4ee534a
25 changed files with 10412 additions and 0 deletions
265
bin/.claude/skills/ddd-model/rules/clean-arch.md
Normal file
265
bin/.claude/skills/ddd-model/rules/clean-arch.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# 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)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue