# 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