Primitive Obsession

Dec 28, 2025 · 2 min read

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
https://nattrio.dev/posts/feed.xml