primitive obsession

**Primitive Obsession** is a **code smell where domain concepts with semantic meaning, validation requirements, and associated behavior are represented using primitive types** — `String`, `int`, `float`, `boolean`, or simple arrays — **instead of small, focused domain objects** — creating code where "a phone number" is just any string, "a price" is just any floating-point number, and "a user ID" is interchangeable with "a product ID" at the type level, eliminating the compile-time safety, centralized validation, and encapsulated behavior that dedicated domain types provide. **What Is Primitive Obsession?** Primitive Obsession manifests in identifiable patterns: - **Identifier Confusion**: `user_id: int` and `product_id: int` are both integers — accidentally passing one where the other is expected is a type-safe operation that silently corrupts data. - **String Abuse**: `phone: str`, `email: str`, `zip_code: str`, `credit_card: str` — all strings, each with completely different validation rules, formatting requirements, and behavior, treated identically by the type system. - **Monetary Values as Floats**: `price: float` represents money with floating-point arithmetic, which cannot represent decimal currency values exactly (0.1 + 0.2 ≠ 0.3 in IEEE 754), leading to financial calculation errors and rounding bugs. - **Status Codes as Strings/Ints**: `status = "active"` or `status = 1` rather than `OrderStatus.ACTIVE` — no compile-time guarantee that only valid statuses are assigned, no IDE autocomplete, no refactoring safety. - **Configuration as Primitives**: Functions accepting `host: str, port: int, timeout: int, retry_count: int, use_ssl: bool` rather than a `ConnectionConfig` object. **Why Primitive Obsession Matters** - **Type Safety Loss**: When user IDs and product IDs are both `int`, the type system cannot prevent `delete_product(user_id)` from compiling. Wrapper types (`UserId(int)`, `ProductId(int)`) make this a compile-time error rather than a silent runtime data corruption. - **Scattered Validation**: Phone number validation, email format checking, ZIP code pattern matching — each appears at every point where the primitive is accepted rather than once in the domain type's constructor. This guarantees validation inconsistency: some call sites validate, others don't, and the rules diverge over time. - **Lost Behavior Opportunities**: A `Money` class should know how to add itself to other `Money` objects of the same currency, format itself for display, convert between currencies, and compare values. A `float` provides none of this — the behavior is scattered across the codebase as utility functions operating on raw floats. - **Documentation Through Types**: `def charge(amount: Money, recipient: AccountId) -> TransactionId` is self-documenting — the types explain what each parameter means and what is returned. `def charge(amount: float, recipient: int) -> int` requires reading the docstring or guessing. - **Refactoring Safety**: If "user ID" changes from integer to UUID, a `UserId` wrapper type requires changing the definition once. A raw `int: user_id` requires a global search-and-replace that may affect unrelated integer fields with the same name. **The Strangler Pattern for Primitive Obsession** Martin Fowler's Tiny Types approach: create minimal wrapper classes for each semantic concept, initially just wrapping the primitive with validation: ```python # Before: Primitive Obsession def create_user(email: str, age: int, phone: str) -> int: if "@" not in email: raise ValueError("Invalid email") if age < 0 or age > 150: raise ValueError("Invalid age") ... # After: Domain Types @dataclass(frozen=True) class Email: value: str def __post_init__(self): if "@" not in self.value: raise ValueError(f"Invalid email: {self.value}") @dataclass(frozen=True) class Age: value: int def __post_init__(self): if not (0 <= self.value <= 150): raise ValueError(f"Invalid age: {self.value}") @dataclass(frozen=True) class UserId: value: int def create_user(email: Email, age: Age, phone: PhoneNumber) -> UserId: ... # Validation has already happened in the domain type constructors ``` **Common Primitive Obsessions and Their Replacements** | Primitive | Replacement | Benefits | |-----------|-------------|---------| | `float` for money | `Money(amount, currency)` | Exact decimal arithmetic, currency safety | | `str` for email | `Email(address)` | Validated format, normalization | | `int` for user ID | `UserId(int)` | Type safety, prevents ID confusion | | `str` for status | `OrderStatus` enum | Exhaustive pattern matching, autocomplete | | `str` for URL | `URL(str)` | Validated format, path extraction | | `str` for phone | `PhoneNumber(str)` | E.164 normalization, formatting | **Tools** - **SonarQube**: Detects Primitive Obsession patterns in multiple languages. - **IntelliJ IDEA**: "Introduce Value Object" refactoring suggestion for recurring primitive groups. - **Designite (C#/Java)**: Design smell detection covering Primitive Obsession. - **JDeodorant**: Java-specific detection with automated refactoring support. Primitive Obsession is **fear of small objects** — the reluctance to create dedicated types for domain concepts that results in a flat, semantically undifferentiated model where every concept is "just a string" or "just an integer," trading type safety, centralized validation, and encapsulated behavior for the illusion of simplicity that ultimately costs far more in scattered validation, silent type errors, and missed business logic concentration opportunities.

Go deeper with CFSGPT

Get AI-powered deep-dives, save terms, and run advanced simulations — free account.

Create Free Account