--- id: wiki-2026-0508-encapsulation-of-domain-invarian title: Encapsulation of Domain Invariants category: 10_Wiki/Topics status: verified canonical_id: self aliases: [domain invariant, value object, aggregate, DDD invariant, branded type] duplicate_of: none source_trust_level: A confidence_score: 0.93 verification_status: applied tags: [software-engineering, ddd, domain-driven-design, invariant, value-object, type-driven] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript / Python framework: DDD --- # Encapsulation of Domain Invariants ## 매 한 줄 > **"매 business rule 의 type / object 의 enforce"**. Evans DDD. 매 'parse, don't validate' (Wlaschin). 매 invariant = 매 always true. 매 value object + aggregate 의 의 매 protect. 매 modern: 매 branded type + Zod + Result type. ## 매 핵심 ### 매 invariant type - **Class invariant**: 매 instance 의 always true. - **Aggregate invariant**: 매 group 의 consistent. - **System invariant**: 매 cross-aggregate. - **Temporal invariant**: 매 lifecycle 의 phase. ### 매 'Parse, don't validate' - 매 raw input → 매 typed value object (parse). - 매 type 의 의 의 의 invariant 의 carry. - 매 every code 매 validate 의 X. ### 매 응용 1. **Email / phone**: 매 type-safe value. 2. **Money**: 매 amount + currency. 3. **Identifier**: 매 UserId vs OrderId. 4. **Range**: 매 age 0-150. 5. **Enum**: 매 Status (limited). 6. **Aggregate**: 매 Order + LineItems consistent. ### 매 modern variant - **TypeScript branded type**. - **Zod runtime parse**. - **Result / Either type**. - **Smart constructor**. - **Refinement type** (advanced). ## 💻 패턴 ### Value object (TypeScript) ```typescript class Email { private constructor(private readonly value: string) {} static parse(raw: string): Email | Error { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) return new Error('Invalid email'); return new Email(raw.toLowerCase().trim()); } toString() { return this.value; } equals(o: Email) { return this.value === o.value; } } ``` ### Branded type ```typescript type Email = string & { readonly __brand: 'Email' }; type UserId = string & { readonly __brand: 'UserId' }; function parseEmail(raw: string): Email { if (!isValidEmail(raw)) throw new Error('Invalid'); return raw as Email; } // 매 type-level safety function send(to: Email) { ... } send(userId); // ❌ Type error send(parseEmail('a@b.com')); // ✅ ``` ### Money (currency-aware) ```typescript class Money { private constructor( public readonly amount: number, // 매 cents (integer) public readonly currency: 'USD' | 'EUR' | 'JPY', ) {} static of(amount: number, currency: Money['currency']): Money { if (!Number.isInteger(amount)) throw new Error('Use cents'); if (amount < 0) throw new Error('Negative not allowed'); return new Money(amount, currency); } plus(other: Money): Money { if (this.currency !== other.currency) throw new Error('Mixed currency'); return new Money(this.amount + other.amount, this.currency); } } ``` ### Aggregate (DDD Order) ```typescript class Order { private constructor( private id: OrderId, private items: LineItem[], private status: OrderStatus, ) { this.checkInvariants(); } private checkInvariants() { if (this.items.length === 0 && this.status === 'PLACED') { throw new Error('Placed order must have items'); } const total = this.totalCents(); if (total < 0) throw new Error('Invariant: non-negative total'); } addItem(item: LineItem) { if (this.status !== 'DRAFT') throw new Error('Cannot add to non-draft'); this.items.push(item); this.checkInvariants(); } place() { if (this.items.length === 0) throw new Error('Empty order'); this.status = 'PLACED'; this.checkInvariants(); } } ``` ### Zod (runtime parse) ```typescript import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid().brand('UserId'), email: z.string().email(), age: z.number().int().min(0).max(150), }); type User = z.infer; function fromInput(raw: unknown): User { return UserSchema.parse(raw); // 매 throws if invalid } ``` ### Result type (no exceptions) ```typescript type Result = { ok: true; value: T } | { ok: false; error: E }; class Email { static parse(raw: string): Result { if (!isValid(raw)) return { ok: false, error: 'Invalid email' }; return { ok: true, value: new Email(raw) }; } } const r = Email.parse(input); if (!r.ok) return alert(r.error); send(r.value); ``` ### Smart constructor (Python) ```python from dataclasses import dataclass @dataclass(frozen=True) class Email: value: str @classmethod def create(cls, raw: str) -> 'Email': if '@' not in raw: raise ValueError('Invalid email') return cls(value=raw.lower().strip()) # 매 Pydantic v2 from pydantic import BaseModel, EmailStr, Field class User(BaseModel): email: EmailStr age: int = Field(ge=0, le=150) ``` ### Range type ```typescript class Age { private constructor(public readonly value: number) {} static of(n: number): Age { if (!Number.isInteger(n) || n < 0 || n > 150) throw new Error('Invalid age'); return new Age(n); } } ``` ### State machine (lifecycle) ```typescript type DraftOrder = { status: 'DRAFT'; items: LineItem[] }; type PlacedOrder = { status: 'PLACED'; items: LineItem[]; placedAt: Date }; type ShippedOrder = { status: 'SHIPPED'; items: LineItem[]; shippedAt: Date }; type Order = DraftOrder | PlacedOrder | ShippedOrder; function place(o: DraftOrder): PlacedOrder { if (o.items.length === 0) throw new Error('Empty'); return { status: 'PLACED', items: o.items, placedAt: new Date() }; } // 매 type 의 transition 의 enforce ``` ### Aggregate transactional boundary ```typescript async function placeOrder(orderId: OrderId) { await db.transaction(async tx => { const order = await tx.order.find(orderId); // 매 lock order.place(); // 매 invariant check await tx.order.save(order); }); } ``` ### Refinement check (advanced) ```typescript // 매 TypeScript 4.9+ — satisfies const config = { retries: 3, timeout: 5000, } satisfies { retries: number; timeout: number }; ``` ### Avoid primitive obsession ```typescript // 매 ❌ function transfer(from: string, to: string, amount: number, currency: string) { ... } transfer('user1', '5000', 'EUR', 100); // 매 args swapped! // 매 ✅ function transfer(from: UserId, to: UserId, amount: Money) { ... } // 매 type system 의 prevent ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Primitive obsession | Branded / value object | | Complex constraint | Smart constructor + Result | | Schema input | Zod / Pydantic | | Multi-field rule | Aggregate | | Lifecycle | State machine type | | Currency / measure | Value object with unit | **기본값**: 매 input boundary parse + 매 branded ID + 매 value object for measures + 매 aggregate for invariant + 매 state-machine type for lifecycle. ## 🔗 Graph - 부모: [[Domain-Driven-Design]] · [[Encapsulation-and-Information-Hiding]] - 변형: [[Value-Object]] · [[Aggregate]] · [[Smart-Constructor]] - 응용: [[Type-Driven-Design]] · [[Branded-Types]] - Adjacent: [[Anaemic Domain Model]] · [[Parse, Don't Validate]] · [[Refinement-Type]] · [[Result Type]] ## 🤖 LLM 활용 **언제**: 매 domain-rich. 매 type-safe API. 매 critical invariant. **언제 X**: 매 CRUD-only. 매 throwaway. ## ❌ 안티패턴 - **Primitive obsession**: 매 string 매 string. - **Validate everywhere**: 매 once parse → trust type. - **Anemic value object**: 매 getter/setter only. - **Cross-aggregate transaction**: 매 boundary 의 violate. - **Nullable invariant**: 매 if-checking 의 cascade. ## 🧪 검증 / 중복 - Verified (Evans DDD, Wlaschin Domain Modeling, Pydantic v2). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-04-20 | Auto-reinforced | | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — DDD invariant + 매 value object / branded / Zod / Result / state code |