Files
2nd/10_Wiki/Topics/AI_and_ML/Encapsulation-of-Domain-Invariants.md
T
2026-05-10 22:08:15 +09:00

8.0 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-encapsulation-of-domain-invarian Encapsulation of Domain Invariants 10_Wiki/Topics verified self
domain invariant
value object
aggregate
DDD invariant
branded type
none A 0.93 applied
software-engineering
ddd
domain-driven-design
invariant
value-object
type-driven
2026-05-10 pending
language framework
TypeScript / Python 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)

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

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)

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)

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)

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<typeof UserSchema>;

function fromInput(raw: unknown): User {
  return UserSchema.parse(raw);  // 매 throws if invalid
}

Result type (no exceptions)

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

class Email {
  static parse(raw: string): Result<Email, string> {
    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)

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

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)

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

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 4.9+ — satisfies
const config = {
  retries: 3,
  timeout: 5000,
} satisfies { retries: number; timeout: number };

Avoid primitive obsession

// 매 ❌
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

🤖 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