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: intandproduct_id: intare 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: floatrepresents 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"orstatus = 1rather thanOrderStatus.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: boolrather than aConnectionConfigobject.
Why Primitive Obsession Matters
- Type Safety Loss: When user IDs and product IDs are both
int, the type system cannot preventdelete_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
Moneyclass should know how to add itself to otherMoneyobjects of the same currency, format itself for display, convert between currencies, and compare values. Afloatprovides 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) -> TransactionIdis self-documenting — the types explain what each parameter means and what is returned.def charge(amount: float, recipient: int) -> intrequires reading the docstring or guessing. - Refactoring Safety: If "user ID" changes from integer to UUID, a
UserIdwrapper type requires changing the definition once. A rawint: user_idrequires 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:
# 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.
Explore 500+ Semiconductor & AI Topics
From EUV lithography to CUDA optimization — search the full knowledge base or chat with our AI assistant.