Primitive Obsession
A code smell where primitive types like strings, integers and booleans represent domain concepts that deserve their own small types.
Also known as: Primitive Obsession Code Smell, Domain Primitives
Category: Software Development
Tags: software-design, software-development, programming, code-quality, refactoring, domain-modeling, anti-patterns
Explanation
An email address is a `string`. A price is a `number`. A user ID is a `string` too, and so is an order ID. Everything compiles. Then one day, someone passes an order ID where a user ID was expected, or adds euros to dollars, and nothing stops them.
Martin Fowler listed Primitive Obsession as a code smell in Refactoring. The smell isn't the use of primitives itself. It's using them for concepts that carry rules, units or meaning of their own.
**Signs you're dealing with it**:
- Functions with several parameters of the same primitive type: `transfer(fromId: string, toId: string, amount: number)`
- Validation of the same value repeated in many places (e.g., checking email format in the form, the API and the database layer)
- Magic strings and numbers for states and types: `status === "actv"`
- Money stored as a float, with the currency in a separate variable (or nowhere)
- Comments explaining what a value really is: `timeout: number // in milliseconds`
- Groups of primitives that always travel together (e.g., `street`, `city`, `zip`, `country`)
**Why it hurts**:
- The compiler can't tell different concepts apart, so mix-ups go unnoticed
- Rules get scattered and duplicated instead of living in one place
- Invalid values can exist anywhere in the system
- The code doesn't speak the language of the domain (see ubiquitous language)
**How to fix it**:
1. **Replace the primitive with a small type**: `EmailAddress`, `UserId`, `Money`, `Duration` (see value objects)
2. **Validate once, at creation**: the type's constructor rejects invalid values, so every instance is valid (see Parse, Don't Validate)
3. **Group related primitives** into a single object (e.g., `Address`, `DateRange`)
4. **Replace magic strings with enums or sum types** for states and categories
5. **Use your language's lightweight options**: newtypes in Rust and Haskell, branded types in TypeScript, value classes in Kotlin, records in Java and C#
The book Secure by Design calls these small types "domain primitives", and shows how they block entire categories of attacks: if invalid input can't become an `EmailAddress`, it can't reach the code that trusts one.
Keep in mind that this is about domain concepts. A loop counter can stay an `int`.
Related Concepts
← Back to all concepts