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:
- Exposure of primitive types:
String,int,f64etc. do not express domain concepts - Construction-time validation: Are invariants validated in the constructor/factory?
- Immutability: field is
privateandfinal/readonly/immutable - Encapsulation: Is access to internal state restricted (
breachEncapsulationOfnaming) - 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 levelwhen-to-wrap-primitives: Criteria for deciding whether to wrap primitive typesdomain-building-blocks: Design of value objects composed of domain primitives