SuperAPI AI Directory

when-to-wrap-primitives

Coding & development

View repository

Updated 10-16 07:23

About this skill

Primitive Type Wrapping Decision Guide

Whether or not to wrap a primitive type should be decided bycost-effectiveness. Wrapping blindly and not wrapping at all are both design failures.

Premise: There is not just one definition of Value Object

The term "Value Object" means different things depending on the context. This is a major cause of confusion in discussions and reviews.

Definition Source Scope Core
General definition Wikipedia, etc. Broadest An object whose equality is based on value rather than ID
PofEAA definition Martin Fowler Implementation pattern A small object whose equality is determined by value rather than ID. Immutability is recommended to avoid the aliasing problem
DDD definition Eric Evans Domain modeling A domain object that has all the characteristics of the PofEAA version, and in addition measures, quantifies, and explains domain concepts, and has invariants and side-effect-free behavior

The DDD version extends the PofEAA version

The DDD version VO and the PofEAA version VO are not independent concepts; they are in aspecialization relationship.

Characteristic PofEAA VO DDD VO
Equality by value Required Required (inherited)
Immutability Recommended Required (strengthened)
Domain invariants ー Required (added)
Domain behavior ー Required (added)

In other words:

  • DDD VO IS-A PofEAA VO → All DDD VOs are also PofEAA VOs
  • PofEAA VO IS-A DDD VO → Does not hold (a Ruby Hash is a PofEAA VO but not a DDD VO)

The DDD versionpresupposes and includesthe PofEAA version's characteristics of "being judged equal by value" and "being immutable", and adds domain-specific requirements. When viewing the two definitions side by side, it is easy to overlook that the DDD version also has the characteristics of the PofEAA version, so be careful.

Within a team, contexts should be made explicit and used properly, such as "PofEAA's VO" and "DDD's VO".

Position of this skill

This skill focuses on the practical decision of "whether primitive types should be wrapped in domain-specific types." Without entering the debate over the definition of VO, it judges bywhether the concrete benefits obtained by wrapping are worth the cost.

Two anti-patterns

Primitive Obsession (obsession with primitive types)

Everything is represented as String/int/float, and domain constraints do not appear in the code.

fn transfer(from: String, to: String, amount: f64)
// fromとtoを取り違えてもコンパイルが通る
// amountが負でもコンパイルが通る
// 通貨の概念がない

Symptoms:

  • Two or more arguments of the same primitive type appear in a row
  • Validation logic is scattered across callers
  • The implicit assumption that "this string is supposed to be an email address"
  • Mixing up units (meters and feet, yen and dollars)

Value Object Obsession (Excessive Wrapping)

Mechanically wrapping every primitive type in a class, which only increases complexity.

class CustomerFirstName(value: String)
class CustomerLastName(value: String)
class ShippingFirstName(value: String)  // CustomerFirstNameと何が違う?
class ShippingLastName(value: String)   // 同上

Symptoms:

  • A large number of "mere wrappers" exist that have no invariants or domain logic
  • Different types are created for each context, but the validation inside is identical
  • There is more type-conversion boilerplate than domain logic
  • New members get lost in a forest of types

Decision Framework

Five Criteria for Decision

Evaluate whether a primitive type should be wrapped using the following criteria. If even one is a strong Yes, consider wrapping. If multiple are Yes, it is strongly recommended. If all are No, wrapping is unnecessary.

Criterion 1: Are there domain invariants?

Does that value have "constraints that must always be satisfied"?

Example Invariant Decision
Email address RFC-compliant format Wrap
Amount of money Non-negative, combination with currency Wrap
Age Range of 0 to 150 Wrap
Log message No particular constraints No wrapping needed
Temporary loop counter None No wrapping needed

Criterion 2: Is there a risk of mix-up?

Do multiple arguments of the same primitive type appear in a row, such that the compiler cannot detect it even if they are mixed up?

// ❌ userIdとorderIdは両方String。取り違えても気づかない
fn find_order(user_id: String, order_id: String)

// ✅ 型が異なるのでコンパイラが検出する
fn find_order(user_id: UserId, order_id: OrderId)

ID types in particular cause great damage when mixed up. IDs for different concepts should be distinguished by type.

Criterion 3: Can domain operations be aggregated?

Do operations related to that value (addition, comparison, conversion, etc.) exist as domain logic?

Example Domain operation Decision
Money Addition (same currency only), currency conversion, comparison Wrap
DateRange Duplicate determination, containment determination, period calculation Wrap
A mere name string None in particular Consider carefully

Criterion 4: Are multiple values inseparable?

Do two or more values always have meaning as a set?

// ❌ 金額と通貨が別々に渡される → 不整合の可能性
fn price(amount: f64, currency: String)

// ✅ 不可分な値を1つの型にまとめる
fn price(money: Money)

Criterion 5: Are there multiple places where it is used?

Is that type used in multiple contexts (modules, layers, functions)?

Situation Judgment
Used by 3 or more modules High value in wrapping
Used by 2 modules Consider in combination with other criteria
Used within only 1 function No wrapping needed (YAGNI)

Decision flow

プリミティブ型を使おうとしている
    │
    ├─ 不変条件がある? ─── Yes ──→ ラップする
    │                                (domain-primitives-and-always-valid スキル参照)
    │
    ├─ 取り違えリスクがある? ─── Yes ──→ ラップする(特にID型)
    │
    ├─ ドメイン操作がある? ─── Yes ──→ ラップする
    │
    ├─ 複数値が不可分? ─── Yes ──→ 複合型としてラップする
    │
    ├─ 利用箇所が多い? ─── Yes ──→ 他の基準と合わせて検討
    │
    └─ すべて No ──→ プリミティブ型のままでよい

Degree of wrapping: 3 levels

Not everything needs to be a "full-spec type." Choose the appropriate granularity according to the situation.

Level 1: Type alias (lightest)

When there are no invariants, but the purpose is preventing mix-ups or improving readability.

// TypeScript
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
// Rust(newtype)
pub struct UserId(String);
pub struct OrderId(String);

Applicable situations: No invariants, main purpose is preventing mix-ups

Level 2: Type with construction-time validation

There are invariants, preventing instantiation of invalid values.

pub struct Email(String);

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()))
    }
}

Applicable situations: There are clear invariants

Level 3: Domain type with behavior

Aggregate invariants + domain operations.

pub struct Money { amount: Decimal, currency: Currency }

impl Money {
    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)
    }
}

Applicable situations: There are invariants + domain operations that belong there

Options based on language characteristics

The means of wrapping is not limited to making a class. Choose the appropriate means according to the language features.

Language Lightweight means Full wrapping
Rust newtype(struct Foo(T)) newtype + impl
TypeScript Branded Types class with private constructor
Kotlin value class data class
Scala opaque type / value class case class
Go type Foo string struct + constructor function
Java — (no lightweight means) record / final class
Python NewType(typing) dataclass(frozen=True)

Principle: If there are no invariants and the only purpose is preventing mix-ups, use the lightest means available in that language.

Review checklist

Detecting "should be wrapped but isn't"

  • Are two or more arguments of the same primitive type lined up?
  • Is validation scattered on the caller side?
  • Is there an implicit assumption that "this string should be X"?
  • Are IDs of different concepts represented by the same type (String, etc.)?
  • Are inseparable pairs of values (amount + currency, etc.) separate arguments?

Detecting "over-wrapping"

  • Is there a "mere wrapper" with neither invariants nor domain operations?
  • Is there more type-conversion boilerplate than domain logic?
  • Are types separated by context, but the validation inside is identical?
  • Are you going out of your way to create a type used in only one place?
  • Does the number of types exceed the team's cognitive load?

How to use it alongside related skills

Skill Focus When to use
This skill Judging whether or not to wrap When making the decision "should I create this type?"
domain-primitives-and-always-valid Design and implementation when wrapping Concrete design after deciding to wrap
domain-building-blocks General design of VO/Entity/Aggregate, etc. When designing the overall structure of the domain model
parse-dont-validate Conversion that holds validation results in the type When reviewing validate→parse conversion

Terminology organization for discussion

When discussing as a team,do not confuse patterns (design concepts) with implementation techniquesthis is important.

Patterns (what they are)

Pattern Source Definition Relationship
Value Object(PofEAA) Fowler An object whose equality is determined by value. Immutability is recommended Base
Value Object(DDD) Evans Inherits from PofEAA VO and adds domain invariants and behavior PofEAA VOのextends
Domain Primitive Johnsson et al. A domain-specific smallest-unit type equipped with construction-time validation, immutability, and self-containedness Derived from Secure by Design

Implementation techniques (how to build it)

Technique Characteristics Example
Branded Type / Newtype Distinguished at the type level, but with no runtime validation Rust: struct UserId(String)
Smart Constructor Validates invariants at construction time. Invalid values cannot be created Email::new(s) -> Result<Email, Error>
Domain type with behavior Encapsulates invariants + domain operations Money::add(&self, other) -> Result<Money, Error>

Correspondence between patterns and implementation techniques

Pattern Typical implementation techniques
PofEAA VO Can be realized with either technique (just implement value equality)
DDD VO Smart Constructor or domain type with behavior
Domain Primitive Smart Constructor (since it is the smallest unit, it often has little behavior)

Recommendation: Do not speak only in terms of pattern names, such as "this should be a Value Object"; rather, like "this value has domain invariants, so it should be protected with a Smart Constructor" "there is a risk of mix-up, so the types should be distinguished with a Newtype",state the reason and the implementation technique togetherstate it.

References

  • Dan Bergh Johnsson et al. "Secure by Design" - the original source of Domain Primitives
  • Martin Fowler "Patterns of Enterprise Application Architecture" - the definition of Value Object in PofEAA
  • Eric Evans "Domain-Driven Design" - the definition of Value Object in DDD
  • J. B. Rainsberger "Demystifying the Dependency Inversion Principle" - reference to Primitive Obsession
  • ThoughtWorks "Object Calisthenics" - source of the "Wrap All Primitives" rule (for practice, not a production rule)

Related skills (recommended for concurrent reading)

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

  • domain-primitives-and-always-valid: Concrete design patterns after deciding to wrap
  • parse-dont-validate: The parse pattern applied when wrapping
  • domain-building-blocks: The design of the value object to which the wrapped primitive belongs