Code smellBloaters
Primitive Obsession
Domain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere.
- Also known as
- Stringly Typed Code
Signs
- The same validation (
/^[^@]+@[^@]+$/,amount >= 0,code.length === 3) appears in many places. - Function signatures read
(string, string, number, string)and callers swap arguments without the compiler noticing. - Constants like
STATUS_ACTIVE = 2or"pending"driveifandswitchstatements. - Money is a
numberof unknown unit (dollars or cents) and currency lives in a separate variable, or nowhere. - Arrays or tuples carry fixed positions with meaning:
row[0]is the name,row[3]is the price.
Why it happens
Primitives are already there and cost nothing to type. Creating a class for "only an email" feels like overkill when the first use appears. Later the email gains normalization, the amount gains a currency, and the status gains transitions, but by then the primitive is threaded through dozens of signatures and nobody wants to touch them all.
Example
A transfer function where every argument is a primitive.
function transfer(fromIban: string, toIban: string, amount: number, currency: string): void {
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(fromIban)) throw new Error("bad from IBAN");
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(toIban)) throw new Error("bad to IBAN");
if (amount <= 0 || !Number.isInteger(amount)) throw new Error("amount must be positive cents");
if (!["EUR", "USD", "GBP"].includes(currency)) throw new Error("unsupported currency");
ledger.post(fromIban, -amount, currency);
ledger.post(toIban, amount, currency);
}
transfer("DE89370400440532013000", "FR1420041010050500013M02606", 2500, "EUR");With value objects, each rule lives in one constructor, and invalid values cannot reach transfer at all.
class Iban {
private constructor(readonly value: string) {}
static parse(raw: string): Iban {
const v = raw.replace(/\s+/g, "").toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(v)) throw new Error(`bad IBAN: ${raw}`);
return new Iban(v);
}
}
type Currency = "EUR" | "USD" | "GBP";
class Money {
constructor(readonly cents: number, readonly currency: Currency) {
if (!Number.isInteger(cents)) throw new Error("cents must be an integer");
}
negate(): Money { return new Money(-this.cents, this.currency); }
}
function transfer(from: Iban, to: Iban, amount: Money): void {
if (amount.cents <= 0) throw new Error("transfer amount must be positive");
ledger.post(from, amount.negate());
ledger.post(to, amount);
}Treatment
- If a single primitive carries rules or formatting (email, IBAN, SKU, percentage), use Replace Data Value with Object.
- If several primitives always travel together (amount and currency, lat and lng), use Introduce Parameter Object, or Preserve Whole Object when the caller already has the owning object.
- If a coded value only identifies a kind and has no behavior, use Replace Type Code with Class, or a string literal union or enum in TypeScript.
- If the code value changes behavior, use Replace Type Code with Subclasses, or Replace Type Code with State/Strategy when the type can change at runtime.
- If an array is used as a record with positional meaning, use Replace Array with Object.
Payoff
- Validation and formatting live in one place, so they cannot drift.
- The type system catches swapped arguments and unit mix-ups.
- New behavior (currency conversion, IBAN masking) has an obvious home.
- Signatures document themselves:
transfer(from: Iban, to: Iban, amount: Money).
When to ignore
- A value that has no rules, no formatting and no unit, such as a free-text comment.
- Throwaway scripts and one-off migrations.
- Hot paths where allocating an object per value matters. In TypeScript, a branded type (
string & { __brand: "Iban" }) gives compile-time safety with no runtime cost; in Go and Rust, a named type over the primitive is free.
Agent checklist
- The value object validates in its constructor or factory, and there is no public way to build an invalid one.
- Value objects are immutable and compare by value, not identity.
- Parsing happens at the system boundary (HTTP handler, file reader), and inner code receives the typed value.
- Duplicate validation scattered through callers is deleted, not left alongside the new type.
- Serialization (JSON, database columns) still produces the same wire format, verified by tests.
Treatments
- Replace Data Value with ObjectTurn a primitive field that has grown rules and companions into a small class that owns its data and behavior.
- Introduce Parameter ObjectReplace a group of parameters that always travel together with one object type, then move behavior on that group into it.
- Preserve Whole ObjectPass the object itself instead of several values pulled out of it, so the method signature stays stable as needs change.
- Replace Type Code with ClassReplace a set of string or number codes that don't affect behavior with a dedicated type, so only valid codes can be used.
- Replace Type Code with SubclassesTurn each value of an immutable type code that drives conditionals into a subclass, and move the branch logic into overrides.
- Replace Type Code with State/StrategyMove behavior that depends on a type code into separate state or strategy objects that the host holds and can swap at runtime.
- Replace Array with ObjectReplace an array whose positions hold different kinds of data with an object that has a named field for each.
See also
- Data ClumpsThe same group of variables travels together through fields and parameters, signaling a missing object that should own them.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
- Long Parameter ListA function takes so many parameters that callers mix up their order and every new option changes every call site.
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.