Primitive Obsession
Primitive Obsession is a code smell that occurs when primitive types (string, int, bool, float64) are used to represent domain concepts. Instead of modeling meaningful business ideas as types, the code relies on raw primitives everywhere.
Why Primitive Obsession Is a Problem
Using primitives for domain concepts leads to:
- Weak type safety
- Repeated validation logic
- Hidden business rules
- Hard-to-read function signatures
- Higher chance of passing the wrong value to the wrong place
The compiler cannot help you because everything looks the same.
Symptoms
Look for these signals in codebase:
Functions with many string or int parameters
Magic values such as
"ACTIVE","ADMIN","PENDING"Repeated validation logic across layers
IDs, emails, money, or status represented as primitives
Bugs caused by mixing values with the same underlying type
Example
Consider a user creation function:
type User struct {
ID string
Email string
Age int
Status string // "ACTIVE" or "INACTIVE"
}
func NewUser(id string, email string, age int, status string) (*User, error) {
if !strings.Contains(email, "@") {
return nil, errors.New("invalid email")
}
if age < 0 {
return nil, errors.New("invalid age")
}
if status != "ACTIVE" && status != "INACTIVE" {
return nil, errors.New("invalid status")
}
return &User{
ID: id,
Email: email,
Age: age,
Status: status,
}, nil
}
Issues Even though validation exists, the function still accepts raw primitives, so incorrect or invalid values can easily flow through the system.
All parameters are primitives with hidden meaning
Validation logic is embedded in the function
Easy to pass incorrect values
Here's how to refactor it using Domain Types. If a value has business meaning, give it a type.
type UserID string
type Status string
const (
StatusActive Status = "ACTIVE"
StatusInactive Status = "INACTIVE"
)
Then, create constructors that enforce validation rules. Validation should live inside the type, not scattered across the codebase.
type Email string
func NewEmail(email string) (Email, error) {
if !strings.Contains(email, "@") {
return "", errors.New("invalid email")
}
return Email(email), nil
}
type Age int
func NewAge(age int) (Age, error) {
if age < 0 {
return 0, errors.New("invalid age")
}
return Age(age), nil
}
Now, update the NewUser function to use these types:
// Updated User struct
type User struct {
ID UserID
Email Email
Age Age
Status Status
}
func NewUser(id UserID, email Email, age Age, status Status) (*User, error) {
return &User{
ID: id,
Email: email,
Age: age,
Status: status,
}, nil
}
Benefits
- Invalid states are unrepresentable
- Function signatures become self-documenting
- Less defensive code
- Safer refactoring
Key Takeaways
Apply when:
- The value represents a business rule
- Validation logic is repeated
- The concept appears in many places
- Bugs are caused by mixing similar primitives
Do not apply when:
- The value is truly generic or short-lived
- No domain rule exists