SuperAPI AI Directory

domain-primitives-and-always-valid

Security & auditing

View repository

Updated 10-15 22:39

About this skill

Domain Primitives & Always-Valid Domain Model

Do not trust primitive types; enforce invariants with domain-specific types.

Core Principles

Domain Primitives(Secure by Design)

Instead of using primitive types as-is, wrap them in domain-specific types at the smallest unit.

Characteristic Description
Construction-time validation An instance cannot be created with an invalid value
Immutable Cannot be changed once created
Self-contained Has no references to other entities
Aggregation of domain operations Encapsulates operations related to that type
Prevention of argument mix-ups Distinguishes different domain types even when they are the same primitive type

Always-Valid Domain Model

Guarantee through the type system that the domain model is always in a valid state.

オブジェクトが存在する = そのオブジェクトは有効である

The Dangers of Primitive Types

Using primitive types as-isbugs that are only discovered for the first time in productiongives rise to.

1. Invalid values contaminate the system

// ❌ プリミティブ型:無効な値が素通りする
fn transfer(from: &str, to: &str, amount: i64) {
    // 負の金額で送金 → 受取人の残高が減り、送金者の残高が増える!
    db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from);
    db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to);
}

transfer("alice", "bob", -10000);  // コンパイルOK、テストも通る、本番で大損害

2. Argument mix-ups slip through tests

// ❌ 同じ型の引数が並ぶと、取り違えてもコンパイラが検出できない
fn create_user(first_name: &str, last_name: &str, email: &str);

// 姓名を逆に渡している。単体テストでは「動く」ので見逃される
create_user("Smith", "John", "john@example.com");
// → DB: first_name="Smith", last_name="John" 😱

3. They create security holes

// ❌ 検証なしのStringはSQLインジェクションの温床
fn find_user(email: String) -> User {
    db.query(&format!("SELECT * FROM users WHERE email = '{}'", email))
}

find_user("'; DROP TABLE users; --".to_string());  // 💀

4. Confusion between different units

// ❌ 両方ともf64。単位の違いをコンパイラが検出できない
fn calculate_distance(meters: f64, feet: f64) -> f64;

// 火星探査機が墜落した原因(実話:Mars Climate Orbiter, 1999年)
let result = calculate_distance(altitude_in_feet, thrust_in_meters);

Why They Cannot Be Found by Tests

Problem Limitations of tests
Negative amounts Happy-path tests only use positive values
Argument order Since both are strings, no type error occurs
Boundary values It is impossible to test all combinations
Confusion between units Since both are numeric, the calculation works "correctly"

If constrained by types, all of these can be detected at compile time.

Antipattern Detection

If you find the following patterns, consider converting them to Domain Primitives:

❌ fn send_email(to: String, subject: String)  // StringはEmailではない
❌ fn create_user(age: i32)                    // i32は年齢の制約を持たない
❌ fn process_order(amount: f64, currency: String)  // 別々に渡すと不整合の可能性
❌ struct User { email: String }               // 検証なしで無効な値を保持できる
❌ if !is_valid_email(s) { return Err(...) }   // 検証後も同じString型
❌ fn schedule(room: String, start: String, end: String)  // 引数の取り違えが検出できない

The Danger of Argument Mix-ups

When multiple arguments of the same primitive type are lined up,the compiler cannot detect the bug:

// ❌ 引数を取り違えてもコンパイルが通る
fn schedule(room: &str, start_time: &str, end_time: &str);
schedule("10:00", "11:00", "Room A");  // バグだがコンパイルOK

// ✅ 型で区別すればコンパイルエラーで検出
fn schedule(room: MeetingRoom, start: Time, end: Time);
schedule(Time::parse("10:00")?, Time::parse("11:00")?, MeetingRoom::new("Room A")?);
// コンパイルエラー: expected `MeetingRoom`, found `Time`

Design Patterns

1. Smart Constructor

// ❌ 外部から直接構築可能
pub struct Email(pub String);

// ✅ Smart Constructorで検証を強制
mod email {
    pub struct Email(String);  // フィールドはprivate

    impl Email {
        pub fn new(value: &str) -> Result<Self, EmailError> {
            if !value.contains('@') || value.len() <= 3 {
                return Err(EmailError::InvalidFormat);
            }
            Ok(Self(value.to_string()))
        }

        // カプセル化を破る場合は命名で明示
        pub fn breach_encapsulation_of_value(&self) -> &str {
            &self.0
        }
    }
}

2. Encapsulation of composite values

// ❌ 関連する値を別々に渡す
fn calculate_price(amount: f64, currency: &str) -> f64;

// ✅ 複合値を単一の型でカプセル化
pub struct Money {
    amount: Decimal,
    currency: Currency,
}

impl Money {
    pub fn new(amount: Decimal, currency: Currency) -> Result<Self, MoneyError> {
        if amount < Decimal::ZERO {
            return Err(MoneyError::NegativeAmount);
        }
        Ok(Self { amount, currency })
    }

    pub fn add(&self, other: &Money) -> Result<Money, MoneyError> {
        if self.currency != other.currency {
            return Err(MoneyError::CurrencyMismatch);
        }
        Money::new(self.amount + other.amount, self.currency)
    }
}

3. Range-constrained types

// ❌ 任意のi32を受け入れる
fn set_age(age: i32);

// ✅ 有効な範囲のみを表現する型
pub struct Age(u8);

impl Age {
    pub fn new(value: u8) -> Result<Self, AgeError> {
        if value > 150 {
            return Err(AgeError::TooOld);
        }
        Ok(Self(value))
    }
}

4. NonEmpty types

// ❌ 空の可能性があるVec
fn process_items(items: Vec<Item>);

// ✅ 空でないことを型で保証
pub struct NonEmpty<T> {
    head: T,
    tail: Vec<T>,
}

impl<T> NonEmpty<T> {
    pub fn new(items: Vec<T>) -> Option<Self> {
        let mut iter = items.into_iter();
        iter.next().map(|head| NonEmpty {
            head,
            tail: iter.collect(),
        })
    }

    pub fn head(&self) -> &T {
        &self.head  // 常に安全にアクセス可能
    }
}

Decision Flow

プリミティブ型を使おうとしている
    ↓
この値にドメイン固有の制約があるか?
    ├─ Yes → Domain Primitiveを作成
    │    ├─ フォーマット制約 → Smart Constructor + 正規表現/パーサー
    │    ├─ 範囲制約 → 境界チェック付きコンストラクタ
    │    ├─ 複合値 → 関連する値をまとめた型
    │    └─ 非空制約 → NonEmpty<T>
    └─ No → プリミティブ型のままでOK(稀)

Implementation Patterns by Language

Rust

// newtype + From/TryFrom
pub struct UserId(Uuid);

impl TryFrom<&str> for UserId {
    type Error = UserIdError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Uuid::parse_str(value)
            .map(UserId)
            .map_err(|_| UserIdError::InvalidFormat)
    }
}

TypeScript

// Branded Types
declare const __brand: unique symbol;
type Brand<T, B> = T & { [__brand]: B };

type Email = Brand<string, 'Email'>;

function parseEmail(s: string): Email | null {
  if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
    return s as Email;
  }
  return null;
}

Java

public final class Email {
    private final String value;

    private Email(String value) {
        this.value = value;
    }

    public static Email parse(String s) {
        if (s == null || !s.contains("@") || s.length() <= 3) {
            throw new IllegalArgumentException("Invalid email: " + s);
        }
        return new Email(s);
    }

    public String breachEncapsulationOfValue() {
        return value;
    }
}

Review Perspectives

Checkpoints during code review:

  1. Exposure of primitive types: String, int, f64etc. do not express domain concepts
  2. Construction-time validation: Are invariants validated in the constructor/factory?
  3. Immutability: field isprivateandfinal/readonly/immutable
  4. Encapsulation: Is access to internal state restricted (breachEncapsulationOfnaming)
  5. Domain operations: Are related operations consolidated into the type

Application Guidelines

Recommendations

  • ID types (UserId, OrderId, ProductId, etc.)
  • Contact information (Email, PhoneNumber, Address, etc.)
  • Financial information (Money, Currency, Percentage, etc.)
  • Measured values (Temperature, Distance, Weight, etc.)
  • Non-empty collections (NonEmpty)

Avoid over-application

  • Temporary local variables
  • Private internal implementation details
  • Boundaries with external libraries (however, apply types in the conversion layer)

Situations where conversion to primitive types is permitted

When crossing domain boundaries, conversion to primitive types is necessary and legitimate.

ドメイン層(ドメイン固有型を使用)
    │
    │  ← ここで変換(breachEncapsulationOf)
    ↓
境界層(JSON, DB, 外部API)← プリミティブ型が必要
Situation Reason Example
JSON/XML serialization Standard formats only support primitive types {"quantity": 5}
Database persistence RDB column types are primitive INSERT INTO orders (quantity) VALUES (5)
External API integration External systems do not know domain types REST API request/response
Log output A human-readable format is required log::info!("注文数: {}", qty.value())
// ✅ 永続化層での正当な使用例
impl OrderRepository {
    fn save(&self, order: &Order) {
        // ドメイン型 → プリミティブ型への変換は境界層で許容
        let quantity_value = order.quantity().breach_encapsulation_of_value();
        db.execute("INSERT INTO orders (quantity) VALUES (?)", quantity_value);
    }

    fn find(&self, id: OrderId) -> Option<Order> {
        let row = db.query_one("SELECT quantity FROM orders WHERE id = ?", id.value())?;
        // プリミティブ型 → ドメイン型への変換(検証付き)
        let quantity = OrderQuantity::new(row.get("quantity")).ok()?;
        Some(Order::new(id, quantity))
    }
}

Principle: Within the domain layer, enforce domain-specific types thoroughly, and convert only at the moment of crossing boundaries.

Differentiation from related skills

Skills Focus When to use
This skill Type design and construction-time validation When designing a new domain type
parse-dont-validate Conversion of validation results into types When reviewing validate→parse conversion
domain-building-blocks DDD tactical patterns in general When designing entities/aggregates/services

References

  • Dan Bergh Johnsson et al. "Secure by Design" - The original source of Domain Primitives
  • Alexis King "Parse, don't validate" - Theoretical background of the Always-Valid principle
  • Scott Wlaschin "Domain Modeling Made Functional" - Practice of type-driven design
  • Einar Landre "Prefer Domain-Specific Types to Primitive Types" in "97 Things Every Programmer Should Know" - Let the compiler find bugs

Related skills (recommended for concurrent reading)

When using this skill, also refer to the following skills:

  • parse-dont-validate: A design philosophy that guarantees invariants at the type level
  • when-to-wrap-primitives: Criteria for deciding whether to wrap primitive types
  • domain-building-blocks: Design of value objects composed of domain primitives