mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 15:29:34 +01:00
docs: and skills
This commit is contained in:
parent
e4f0665086
commit
ccd4ee534a
25 changed files with 10412 additions and 0 deletions
633
bin/.claude/skills/ddd-model/examples/banking-go.md
Normal file
633
bin/.claude/skills/ddd-model/examples/banking-go.md
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
# Example: Banking Domain
|
||||
|
||||
This example demonstrates DDD modeling for a banking domain, covering all phases of the workflow.
|
||||
|
||||
## Phase 1: Domain Discovery
|
||||
|
||||
### Domain Description
|
||||
A digital banking platform that allows customers to open accounts, make transfers, and earn loyalty rewards.
|
||||
|
||||
### Subdomain Classification
|
||||
|
||||
| Subdomain | Type | DDD Investment |
|
||||
|-----------|------|----------------|
|
||||
| **Accounts** | Core | Full DDD - competitive advantage through account features |
|
||||
| **Transfers** | Core | Full DDD - business-critical payment processing |
|
||||
| **Loyalty** | Core | Full DDD - customer retention differentiator |
|
||||
| **Fees** | Supporting | Simplified DDD - necessary but not differentiating |
|
||||
| **Notifications** | Generic | CRUD - standard email/SMS sending |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Bounded Contexts
|
||||
|
||||
### Context Map
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ BANKING DOMAIN │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ ACCOUNTS │─────>│ TRANSFERS │ │
|
||||
│ │ │ CS │ │ │
|
||||
│ │ - Account │ │ - Transfer │ │
|
||||
│ │ - Balance │ │ - Payment │ │
|
||||
│ │ - Holder │ │ - Status │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ CF │ P │
|
||||
│ v v │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ FEES │ │ LOYALTY │ │
|
||||
│ │ (Supporting) │ │ (Core) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - Fee │ │ - Program │ │
|
||||
│ │ - Schedule │ │ - Tier │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
Legend:
|
||||
CS = Customer-Supplier (Accounts supplies, Transfers consumes)
|
||||
CF = Conformist (Fees conforms to Accounts model)
|
||||
P = Partnership (Transfers and Loyalty evolve together)
|
||||
```
|
||||
|
||||
### Ubiquitous Language
|
||||
|
||||
#### Accounts Context
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| Account | A financial account owned by one or more customers |
|
||||
| Balance | Current amount of money in the account (in minor units) |
|
||||
| Holder | A person with access to the account (Owner, Operator, Viewer) |
|
||||
| Freeze | Temporarily block all debit operations on an account |
|
||||
| Standard Account | Account that cannot have negative balance |
|
||||
| Credit Account | Account with approved credit limit |
|
||||
|
||||
#### Transfers Context
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| Transfer | Movement of money between two accounts |
|
||||
| Internal Transfer | Transfer within the same bank |
|
||||
| Source Account | Account debited in a transfer |
|
||||
| Destination Account | Account credited in a transfer |
|
||||
| Pending | Transfer initiated but not yet completed |
|
||||
| Completed | Transfer successfully processed |
|
||||
| Failed | Transfer that could not be completed |
|
||||
|
||||
#### Loyalty Context
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| Program | Loyalty program a customer is enrolled in |
|
||||
| Tier | Level within the program (Bronze, Silver, Gold, Platinum) |
|
||||
| Points | Accumulated loyalty points |
|
||||
| Earn Rate | Points earned per currency unit spent |
|
||||
| Redemption | Converting points to rewards |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Tactical Modeling
|
||||
|
||||
### Accounts Bounded Context
|
||||
|
||||
#### Aggregate: Account
|
||||
|
||||
```
|
||||
Account Aggregate
|
||||
├── Account (Aggregate Root)
|
||||
│ ├── AccountID (Value Object)
|
||||
│ ├── Balance (Value Object: Money)
|
||||
│ ├── Status (Value Object: Active|Frozen|Closed)
|
||||
│ ├── AccountType (Value Object: Standard|Credit)
|
||||
│ ├── CreditLimit (Value Object: Money)
|
||||
│ └── Holders[] (Entity)
|
||||
│ ├── HolderID (Value Object)
|
||||
│ ├── UserID (Value Object - reference to User aggregate)
|
||||
│ └── Role (Value Object: Owner|Operator|Viewer)
|
||||
│
|
||||
└── Invariants:
|
||||
- Balance >= 0 for Standard accounts
|
||||
- Balance >= -CreditLimit for Credit accounts
|
||||
- At least one Holder with Owner role
|
||||
- Cannot debit Frozen account
|
||||
- Cannot modify Closed account
|
||||
```
|
||||
|
||||
#### Value Objects
|
||||
|
||||
```go
|
||||
// AccountID - unique identifier
|
||||
type AccountID struct { value uuid.UUID }
|
||||
|
||||
// Money - monetary amount with currency
|
||||
type Money struct {
|
||||
amount int64 // cents
|
||||
currency string // ISO 4217
|
||||
}
|
||||
|
||||
// Status - account status
|
||||
type Status struct { value string }
|
||||
var (
|
||||
StatusActive = Status{"active"}
|
||||
StatusFrozen = Status{"frozen"}
|
||||
StatusClosed = Status{"closed"}
|
||||
)
|
||||
|
||||
// AccountType
|
||||
type AccountType struct { value string }
|
||||
var (
|
||||
AccountTypeStandard = AccountType{"standard"}
|
||||
AccountTypeCredit = AccountType{"credit"}
|
||||
)
|
||||
|
||||
// Role - holder role
|
||||
type Role struct { value string }
|
||||
var (
|
||||
RoleOwner = Role{"owner"}
|
||||
RoleOperator = Role{"operator"}
|
||||
RoleViewer = Role{"viewer"}
|
||||
)
|
||||
```
|
||||
|
||||
### Transfers Bounded Context
|
||||
|
||||
#### Aggregate: Transfer
|
||||
|
||||
```
|
||||
Transfer Aggregate
|
||||
├── Transfer (Aggregate Root)
|
||||
│ ├── TransferID (Value Object)
|
||||
│ ├── SourceAccountID (Value Object - reference only!)
|
||||
│ ├── DestinationAccountID (Value Object - reference only!)
|
||||
│ ├── Amount (Value Object: Money)
|
||||
│ ├── Status (Value Object: Pending|Completed|Failed)
|
||||
│ ├── FailureReason (Value Object, optional)
|
||||
│ ├── InitiatedAt (timestamp)
|
||||
│ └── CompletedAt (timestamp, optional)
|
||||
│
|
||||
└── Invariants:
|
||||
- Amount must be positive
|
||||
- Source and Destination must be different
|
||||
- Status transitions: Pending → Completed|Failed only
|
||||
- Cannot modify after Completed or Failed
|
||||
```
|
||||
|
||||
### Loyalty Bounded Context
|
||||
|
||||
#### Aggregate: LoyaltyProgram
|
||||
|
||||
```
|
||||
LoyaltyProgram Aggregate
|
||||
├── LoyaltyProgram (Aggregate Root)
|
||||
│ ├── ProgramID (Value Object)
|
||||
│ ├── CustomerID (Value Object - reference only!)
|
||||
│ ├── Tier (Value Object: Bronze|Silver|Gold|Platinum)
|
||||
│ ├── Points (Value Object)
|
||||
│ ├── EarnRate (Value Object)
|
||||
│ └── PointsHistory[] (Value Object)
|
||||
│ ├── Date
|
||||
│ ├── Amount
|
||||
│ ├── Type (Earned|Redeemed|Expired)
|
||||
│ └── Description
|
||||
│
|
||||
└── Invariants:
|
||||
- Points >= 0
|
||||
- Tier determined by points thresholds
|
||||
- Cannot redeem more points than available
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Invariants
|
||||
|
||||
### Account Aggregate Invariants
|
||||
|
||||
```go
|
||||
// Account aggregate invariants:
|
||||
//
|
||||
// Invariant: Balance cannot be negative for standard accounts
|
||||
// Enforced in: Withdraw(), constructor
|
||||
//
|
||||
// Invariant: Balance cannot exceed credit limit for credit accounts
|
||||
// Enforced in: Withdraw(), constructor
|
||||
//
|
||||
// Invariant: Account must have at least one holder with OWNER role
|
||||
// Enforced in: RemoveHolder(), constructor
|
||||
//
|
||||
// Invariant: Frozen account cannot process debit operations
|
||||
// Enforced in: Withdraw()
|
||||
//
|
||||
// Invariant: Closed account cannot be modified
|
||||
// Enforced in: all mutation methods
|
||||
```
|
||||
|
||||
### Transfer Aggregate Invariants
|
||||
|
||||
```go
|
||||
// Transfer aggregate invariants:
|
||||
//
|
||||
// Invariant: Transfer amount must be positive
|
||||
// Enforced in: constructor
|
||||
//
|
||||
// Invariant: Source and destination accounts must be different
|
||||
// Enforced in: constructor
|
||||
//
|
||||
// Invariant: Status can only transition Pending → Completed or Pending → Failed
|
||||
// Enforced in: Complete(), Fail()
|
||||
//
|
||||
// Invariant: Completed or Failed transfer cannot be modified
|
||||
// Enforced in: all mutation methods
|
||||
```
|
||||
|
||||
### LoyaltyProgram Aggregate Invariants
|
||||
|
||||
```go
|
||||
// LoyaltyProgram aggregate invariants:
|
||||
//
|
||||
// Invariant: Points balance cannot be negative
|
||||
// Enforced in: RedeemPoints()
|
||||
//
|
||||
// Invariant: Tier is determined by points thresholds
|
||||
// Enforced in: EarnPoints() (automatic upgrade), calculateTier()
|
||||
//
|
||||
// Invariant: Cannot redeem more points than available
|
||||
// Enforced in: RedeemPoints()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Generated Code
|
||||
|
||||
### Account Aggregate
|
||||
|
||||
```go
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInsufficientFunds = errors.New("insufficient funds")
|
||||
ErrAccountFrozen = errors.New("account is frozen")
|
||||
ErrAccountClosed = errors.New("account is closed")
|
||||
ErrCannotRemoveLastOwner = errors.New("cannot remove last owner")
|
||||
ErrNegativeAmount = errors.New("amount must be positive")
|
||||
ErrCreditLimitExceeded = errors.New("credit limit exceeded")
|
||||
)
|
||||
|
||||
// AccountAggregate represents a bank account.
|
||||
//
|
||||
// Invariants:
|
||||
// - Balance >= 0 for standard accounts
|
||||
// - Balance >= -CreditLimit for credit accounts
|
||||
// - At least one holder with OWNER role
|
||||
// - Frozen account cannot process debit operations
|
||||
// - Closed account cannot be modified
|
||||
type AccountAggregate struct {
|
||||
id AccountID
|
||||
balance Money
|
||||
holders []Holder
|
||||
status Status
|
||||
accountType AccountType
|
||||
creditLimit Money
|
||||
|
||||
createdAt time.Time
|
||||
updatedAt time.Time
|
||||
events []DomainEvent
|
||||
}
|
||||
|
||||
// NewAccount creates a new standard account.
|
||||
func NewAccount(id AccountID, initialHolder Holder) (*AccountAggregate, error) {
|
||||
if initialHolder.Role() != RoleOwner {
|
||||
return nil, ErrCannotRemoveLastOwner
|
||||
}
|
||||
|
||||
return &AccountAggregate{
|
||||
id: id,
|
||||
balance: MustMoney(0, "USD"),
|
||||
holders: []Holder{initialHolder},
|
||||
status: StatusActive,
|
||||
accountType: AccountTypeStandard,
|
||||
creditLimit: MustMoney(0, "USD"),
|
||||
createdAt: time.Now(),
|
||||
updatedAt: time.Now(),
|
||||
events: make([]DomainEvent, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCreditAccount creates a new credit account with limit.
|
||||
func NewCreditAccount(id AccountID, initialHolder Holder, creditLimit Money) (*AccountAggregate, error) {
|
||||
account, err := NewAccount(id, initialHolder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.accountType = AccountTypeCredit
|
||||
account.creditLimit = creditLimit
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (a *AccountAggregate) ID() AccountID { return a.id }
|
||||
func (a *AccountAggregate) Balance() Money { return a.balance }
|
||||
func (a *AccountAggregate) Status() Status { return a.status }
|
||||
func (a *AccountAggregate) AccountType() AccountType { return a.accountType }
|
||||
func (a *AccountAggregate) CreditLimit() Money { return a.creditLimit }
|
||||
func (a *AccountAggregate) CreatedAt() time.Time { return a.createdAt }
|
||||
func (a *AccountAggregate) UpdatedAt() time.Time { return a.updatedAt }
|
||||
|
||||
// Deposit adds money to the account.
|
||||
func (a *AccountAggregate) Deposit(amount Money) error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
if amount.IsNegativeOrZero() {
|
||||
return ErrNegativeAmount
|
||||
}
|
||||
|
||||
newBalance, err := a.balance.Add(amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.balance = newBalance
|
||||
a.updatedAt = time.Now()
|
||||
a.raise(DepositedEvent{AccountID: a.id, Amount: amount, NewBalance: a.balance})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Withdraw removes money from the account.
|
||||
func (a *AccountAggregate) Withdraw(amount Money) error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
if a.status == StatusFrozen {
|
||||
return ErrAccountFrozen
|
||||
}
|
||||
if amount.IsNegativeOrZero() {
|
||||
return ErrNegativeAmount
|
||||
}
|
||||
|
||||
newBalance, err := a.balance.Subtract(amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check balance constraints based on account type
|
||||
if a.accountType == AccountTypeStandard && newBalance.IsNegative() {
|
||||
return ErrInsufficientFunds
|
||||
}
|
||||
if a.accountType == AccountTypeCredit && newBalance.IsNegative() {
|
||||
if newBalance.Abs().GreaterThan(a.creditLimit) {
|
||||
return ErrCreditLimitExceeded
|
||||
}
|
||||
}
|
||||
|
||||
a.balance = newBalance
|
||||
a.updatedAt = time.Now()
|
||||
a.raise(WithdrawnEvent{AccountID: a.id, Amount: amount, NewBalance: a.balance})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Freeze blocks debit operations on the account.
|
||||
func (a *AccountAggregate) Freeze() error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
a.status = StatusFrozen
|
||||
a.updatedAt = time.Now()
|
||||
a.raise(AccountFrozenEvent{AccountID: a.id})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unfreeze allows debit operations again.
|
||||
func (a *AccountAggregate) Unfreeze() error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
a.status = StatusActive
|
||||
a.updatedAt = time.Now()
|
||||
a.raise(AccountUnfrozenEvent{AccountID: a.id})
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddHolder adds a new holder to the account.
|
||||
func (a *AccountAggregate) AddHolder(holder Holder) error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
a.holders = append(a.holders, holder)
|
||||
a.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveHolder removes a holder from the account.
|
||||
func (a *AccountAggregate) RemoveHolder(holderID HolderID) error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
|
||||
// Check invariant: must keep at least one owner
|
||||
remainingOwners := 0
|
||||
for _, h := range a.holders {
|
||||
if h.ID() != holderID && h.Role() == RoleOwner {
|
||||
remainingOwners++
|
||||
}
|
||||
}
|
||||
if remainingOwners == 0 {
|
||||
return ErrCannotRemoveLastOwner
|
||||
}
|
||||
|
||||
// Remove holder
|
||||
newHolders := make([]Holder, 0, len(a.holders)-1)
|
||||
for _, h := range a.holders {
|
||||
if h.ID() != holderID {
|
||||
newHolders = append(newHolders, h)
|
||||
}
|
||||
}
|
||||
a.holders = newHolders
|
||||
a.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Holders returns a copy of the holders list.
|
||||
func (a *AccountAggregate) Holders() []Holder {
|
||||
result := make([]Holder, len(a.holders))
|
||||
copy(result, a.holders)
|
||||
return result
|
||||
}
|
||||
|
||||
// Events returns and clears pending domain events.
|
||||
func (a *AccountAggregate) Events() []DomainEvent {
|
||||
events := a.events
|
||||
a.events = make([]DomainEvent, 0)
|
||||
return events
|
||||
}
|
||||
|
||||
func (a *AccountAggregate) raise(event DomainEvent) {
|
||||
a.events = append(a.events, event)
|
||||
}
|
||||
```
|
||||
|
||||
### Money Value Object
|
||||
|
||||
```go
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMoneyCurrencyMismatch = errors.New("cannot operate on different currencies")
|
||||
ErrMoneyCurrencyRequired = errors.New("currency is required")
|
||||
ErrMoneyCurrencyInvalid = errors.New("currency must be 3-letter ISO code")
|
||||
)
|
||||
|
||||
// Money represents a monetary amount with currency.
|
||||
// Immutable value object.
|
||||
type Money struct {
|
||||
amount int64 // cents/minor units
|
||||
currency string // ISO 4217
|
||||
}
|
||||
|
||||
// NewMoney creates a new Money value object.
|
||||
func NewMoney(amount int64, currency string) (Money, error) {
|
||||
if currency == "" {
|
||||
return Money{}, ErrMoneyCurrencyRequired
|
||||
}
|
||||
if len(currency) != 3 {
|
||||
return Money{}, ErrMoneyCurrencyInvalid
|
||||
}
|
||||
return Money{amount: amount, currency: currency}, nil
|
||||
}
|
||||
|
||||
// MustMoney creates Money or panics. Use in tests only.
|
||||
func MustMoney(amount int64, currency string) Money {
|
||||
m, err := NewMoney(amount, currency)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// USD creates Money in US Dollars.
|
||||
func USD(cents int64) Money {
|
||||
return MustMoney(cents, "USD")
|
||||
}
|
||||
|
||||
func (m Money) Amount() int64 { return m.amount }
|
||||
func (m Money) Currency() string { return m.currency }
|
||||
|
||||
// Add returns a new Money with the sum.
|
||||
func (m Money) Add(other Money) (Money, error) {
|
||||
if m.currency != other.currency {
|
||||
return Money{}, ErrMoneyCurrencyMismatch
|
||||
}
|
||||
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
|
||||
}
|
||||
|
||||
// Subtract returns a new Money with the difference.
|
||||
func (m Money) Subtract(other Money) (Money, error) {
|
||||
if m.currency != other.currency {
|
||||
return Money{}, ErrMoneyCurrencyMismatch
|
||||
}
|
||||
return Money{amount: m.amount - other.amount, currency: m.currency}, nil
|
||||
}
|
||||
|
||||
// Multiply returns Money multiplied by factor.
|
||||
func (m Money) Multiply(factor int) Money {
|
||||
return Money{amount: m.amount * int64(factor), currency: m.currency}
|
||||
}
|
||||
|
||||
func (m Money) IsNegative() bool { return m.amount < 0 }
|
||||
func (m Money) IsNegativeOrZero() bool { return m.amount <= 0 }
|
||||
func (m Money) IsPositive() bool { return m.amount > 0 }
|
||||
func (m Money) IsZero() bool { return m.amount == 0 }
|
||||
|
||||
func (m Money) Abs() Money {
|
||||
if m.amount < 0 {
|
||||
return Money{amount: -m.amount, currency: m.currency}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Money) GreaterThan(other Money) bool { return m.amount > other.amount }
|
||||
func (m Money) LessThan(other Money) bool { return m.amount < other.amount }
|
||||
|
||||
func (m Money) Equals(other Money) bool {
|
||||
return m.amount == other.amount && m.currency == other.currency
|
||||
}
|
||||
|
||||
func (m Money) String() string {
|
||||
sign := ""
|
||||
amount := m.amount
|
||||
if amount < 0 {
|
||||
sign = "-"
|
||||
amount = -amount
|
||||
}
|
||||
dollars := amount / 100
|
||||
cents := amount % 100
|
||||
return fmt.Sprintf("%s%d.%02d %s", sign, dollars, cents, m.currency)
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Interface
|
||||
|
||||
```go
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrAccountNotFound = errors.New("account not found")
|
||||
|
||||
// AccountRepository defines persistence operations for Account aggregates.
|
||||
type AccountRepository interface {
|
||||
Save(ctx context.Context, account *AccountAggregate) error
|
||||
FindByID(ctx context.Context, id AccountID) (*AccountAggregate, error)
|
||||
FindByHolderUserID(ctx context.Context, userID UserID) ([]*AccountAggregate, error)
|
||||
Exists(ctx context.Context, id AccountID) (bool, error)
|
||||
Delete(ctx context.Context, id AccountID) error
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Validation Checklist
|
||||
|
||||
### Account Aggregate ✅
|
||||
|
||||
- [x] Aggregate Root is the only entry point
|
||||
- [x] Child entities (Holder) accessed via aggregate methods
|
||||
- [x] All changes through aggregate methods (Deposit, Withdraw, etc.)
|
||||
- [x] Invariants documented in comments
|
||||
- [x] Invariants checked in constructor
|
||||
- [x] Invariants checked in all mutation methods
|
||||
- [x] No direct references to other Aggregates (UserID is ID only)
|
||||
- [x] One Aggregate = one transaction boundary
|
||||
|
||||
### Value Objects ✅
|
||||
|
||||
- [x] Money is immutable (no setters)
|
||||
- [x] Money validates in constructor
|
||||
- [x] Money operations return new instances
|
||||
- [x] Equals compares all fields
|
||||
- [x] MustXxx variants for tests
|
||||
|
||||
### Repository ✅
|
||||
|
||||
- [x] Interface in domain layer
|
||||
- [x] Methods operate on aggregates
|
||||
- [x] Context parameter for all methods
|
||||
- [x] Domain-specific errors (ErrAccountNotFound)
|
||||
|
||||
### Clean Architecture ✅
|
||||
|
||||
- [x] Domain layer has no external dependencies
|
||||
- [x] Repository interface in domain, implementation in infrastructure
|
||||
- [x] Domain events for cross-aggregate communication
|
||||
Loading…
Add table
Add a link
Reference in a new issue