Code Smell – Primitive Obsession

Why using int, String or boolean everywhere is a problem and How Java 21 Helps fix it!!

The Problem: Primitive Obsession

This code smell appears when:

  • You overuse primitives (int, double, String, etc.) instead of modeling concepts with domain objects.
  • Business rules are scattered and duplicated.
  • Validation is missed or repeated.
  • Your code becomes harder to understand, validate, or extend.

Bad Example:

public void transfer(String fromAccount, String toAccount, double amount) {
    // business logic
}

When calling this method, we think about,

  • What is a valid account?
  • Can the amount be negative?
  • Is “123” a valid account number?

Let me show you another bad example:

public void registerUser(String firstName, String lastName, String email, String dob, String phone) {
    // validate here? reuse elsewhere?
}
  • Here, name, email, dob and phone are all just Strings.
  • There’s no way to enforce validation, formatting or business rules.
  • Nothing in the method signature tells you what’s expected.

Usual Problems:

No Validation: Every place that uses these values must revalidate them manually

Error-prone APIs: It’s easy to swap arguments by mistake and not know.

Duplicated Logic: Validation logic is copied in many places.

Poor Expressiveness: The method signatures don’t tell you what the values actually represent.

Low Cohesion: Business logic is separated from the data it relates to.

Refactored with Java 21 (Using Record + Validation):

record AccountNumber(String value) {
    public AccountNumber {
        if (!value.matches("\\d{10}")) {
            throw new IllegalArgumentException("Invalid account number");
        }
    }
}

record Money(BigDecimal amount) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
    }
}

public void transfer(AccountNumber from, AccountNumber to, Money amount) {
    // business logic is now clearer, validated by design
}

Clean Code Principles Applied:

Tell, don’t ask — Your code talks in domain terms.

Fail-fast — Validation is part of object creation.

Encapsulation — Business rules live with the data.

Clarity over cleverness — The method signature tells you what’s expected.

Do you think this will “boat” the project?

This is not considered project “bloat” — it’s intentional separation of concerns that brings the following benefits:

  1. Self-validating objects: Logic & validation are encapsulated within the record.
  2. Compiler-level safety: No additional data passing to the method, example: a random string cannot be passed as an email and cannot pass arguments in wrong order.

This also becomes a Domain Driven Desigh (DDD): when you create record for domain concept its ok to increase the project file load.

Having one class per domain concept is actually a Clean Code Principle, especially in:

  • Domain -Driven Design (DDD)
  • Enterprise Java apps
  • Business-critical applications

Still if you think the records are bloating the project, you can also group records:

public class ValueTypes {
    public record Email(String value) {
        public Email {
            if (!value.contains("@")) throw new IllegalArgumentException("Invalid email");
        }
    }

    public record Age(int value) {
        public Age {
            if (value < 0 || value > 120) throw new IllegalArgumentException("Invalid age");
        }
    }
}

Using it:

User user = new User(
    new ValueTypes.Email("me@example.com"),
    new ValueTypes.Age(42)
);

However, Why we should avoid grouping records as much as possible?

Reason 1: Violates Single Responsibility Principle (SRP)

In DDD and Clean Architecture, each class/type should represent one clear concept.

  • If you put multiple records like Email, Age, PhoneNumber inside one outer class (ValueTypes), you’re bundling unrelated domain concepts.
  • This makes the file harder to navigate, harder to test, and less modular.

In contrast, DDD encourages:

  • One concept = one file/class
  • Clear mapping to the Ubiquitous Language used by domain experts

Reason 2: Hurts Discoverability & Reusability

new ValueTypes.Email("x@example.com");  
// instead of just new Email("x@example.com")
  • You’re now forced to access records through a wrapper class.
  • This breaks intuitive naming and packaging.
  • Other classes can’t just import Email — they need to know it lives in ValueTypes.

Reason 3: Domain Concepts Should Be First-Class Citizens

  • In DDD, Email, Age, PhoneNumber aren’t just data — they are important building blocks of your domain.
  • Putting them inside a ValueTypes container says: “They’re just utility types” — which dilutes their importance.

Instead of importing as “import myapp.domain.Email;“, we need to “import myapp.common.ValueTypes.Email;“.

So When Is Grouping Okay?

You can group them only when:

  • You’re in a non-DDD, utility-style codebase
  • The types are simple, used only in one narrow context
  • You’re trying to reduce clutter in a small or medium app

But even then, it’s a tradeoff: convenience vs clarity.

Key Takeaways:

Primitive Obsession leads to fragile, unclear code. Embracing value objects—especially with record types in Java 21—can dramatically improve the clarity, correctness, and maintainability of your code.

So next time you’re tempted to pass around strings and integers, stop and ask: “Should this be a class of its own?”

Asha Ponraj
Asha Ponraj

Data science and Machine Learning enthusiast | Software Developer | Blog Writter

Articles: 89

Leave a Reply

Your email address will not be published. Required fields are marked *