What are the Data Clumps and Primitive Obsession code smells, how are they related, and what refactorings remove them?
answer
- values that always travel together
- deletion test: still meaningful alone?
- stringly-typed / money as double
- missing domain concept
- parse, don't validate — validate in the constructor
basics
~20 sData Clumps: the same group of values keeps travelling together (e.g. street, city, zip). Primitive Obsession: using built-in types like string or int for domain ideas such as money or email. Both are fixed by creating a small named type.
solid answer
~50 s**Data Clumps** — the same two or more data items repeatedly appear together: as adjacent parameters in several signatures, as fields in several classes, as tuple elements. The test is deletion: if removing one item makes the others meaningless, they form a real concept. Refactor with Extract Class (for fields) or Introduce Parameter Object / Preserve Whole Object (for parameters). **Primitive Obsession** — representing domain concepts with built-in types: a phone number as a string, money as a double, a currency as an int code, a state machine as a string enum-by-convention. Consequences: validation is scattered and duplicated, illegal values are representable, unit and type confusion is possible (kilometres passed as miles), and the type system stops helping. Refactor with Replace Primitive with Object (Value Object), Replace Type Code with Subclasses or with Strategy/Polymorphism, Replace Array with Object. They are the same disease: a missing domain concept. Fixing them gives the new type a home for validation, formatting, and the behaviour that was previously scattered across envious callers.
code
pseudocode · 8 lines// Data Clump + Primitive Obsession
book(roomId: string, startY:int, startM:int, startD:int,
endY:int, endM:int, endD:int, price: double)
// After: named concepts carry their own invariants
class DateRange { init(start, end) { require(end >= start) } ; days() ... }
class Money { init(amount: Decimal, currency: Currency) ; plus(other) ... }
book(roomId: RoomId, stay: DateRange, price: Money)go deeper
Define both with a concrete example each (street/city/zip; email as a string) and name the fix as "make a small class for it".
Add the deletion test, name Extract Class / Introduce Parameter Object / Replace Primitive with Object, and explain scattered validation and transposable arguments as the concrete costs.
Discuss value-object semantics (immutability, value equality, validate-in-constructor, parse-don't-validate), unit/ID confusion, money-as-double, and the boundary-conversion strategy.
Frame it as ubiquitous-language modelling: the new type is the place where invariants become unbypassable, and weigh boilerplate, serialization/ORM friction, and hot-path performance against the invariant guarantee.
## Data Clumps **Definition.** Fowler: "Data items tend to be like children; they enjoy hanging around in groups together." When you consistently see the same bunch of values — three fields on several classes, four adjacent parameters on several methods — that bunch is an unnamed concept. **Typical examples.** `startDate`/`endDate`; `street`/`city`/`postcode`/`country`; `amount`/`currency`; `x`/`y`; `host`/`port`/`timeout`/`retries`; `latitude`/`longitude`. **The deletion test.** Delete one item from the group. If the rest immediately stop making sense (an `amount` with no `currency`; a `startDate` with no `endDate`), they belong together. If the rest still make sense on their own, it was coincidental co-occurrence, not a clump. **Why it hurts.** - Every function that takes the clump takes a long parameter list, with transposition risk between same-typed arguments. - Invariants that span the group ("end must be after start") have nowhere to live, so they are re-checked, inconsistently, in many places. - Adding a member to the group means editing every signature and every call site. **Refactorings.** *Extract Class* when the clump is fields on a class; *Introduce Parameter Object* when it is a parameter group; *Preserve Whole Object* when a caller is exploding an object it already holds. Afterwards, look for behaviour to pull in: a `DateRange` naturally acquires `contains()`, `overlaps()`, `days()` — methods previously duplicated in envious callers. ## Primitive Obsession **Definition.** Using the language's built-in types (string, int, float, boolean, arrays, maps) to model domain concepts that deserve their own type. **Common forms.** - **Stringly-typed data**: emails, URLs, phone numbers, IBANs, IDs, ISO country codes — all `string`. - **Money as a floating-point number**: loses the currency, and binary floating point cannot represent decimal fractions exactly, causing rounding drift in totals. - **Type codes**: `int status = 3`, or `"ACTIVE"` compared with string literals, instead of an enum or polymorphic subtype. - **Bare quantities**: a `double` distance whose unit (km? miles?) exists only in a comment — the class of error that destroyed NASA's Mars Climate Orbiter. - **Maps/arrays as records**: `config["timeout"]` instead of a typed settings object; `point[0]`, `point[1]` instead of `x`, `y`. **Why it hurts.** 1. **Illegal states are representable.** Any string can be assigned where an email is expected, so validation must be repeated at every entry point, and someone will forget. 2. **No type safety across concepts.** `userId` and `orderId` are both strings and can be swapped silently; `Miles` and `Kilometres` are both doubles. 3. **No home for behaviour.** Formatting, normalisation, comparison, and arithmetic rules scatter into utility classes and duplicate. 4. **Meaningless APIs.** `f(String, String, String, boolean)` documents nothing. **Refactorings.** - **Replace Primitive with Object** — introduce a **Value Object**: a small immutable type identified by its value rather than an identity, validating in its constructor, with equality by value. Once constructed it can never be invalid — the *parse, don't validate* discipline: convert unvalidated input into a validated type once, at the boundary. - **Replace Type Code with Subclasses / with Strategy / with State** — turns `switch (typeCode)` chains into polymorphism. - **Replace Array with Object**, **Replace Magic Literal with named constant**. ## How the two relate Both are the same underlying defect: **a missing domain concept**. A Data Clump is the concept spread across several *slots*; Primitive Obsession is the concept collapsed into a *single wrong-typed slot*. `amount` + `currencyCode` is a Data Clump; `double amount` alone is Primitive Obsession; both are cured by a `Money` value object. Curing them typically triggers a cascade of improvements: parameter lists shrink, duplicated validation collapses into one constructor, Feature Envy disappears because envious callers can now call methods on the new type, and Long Method bodies shorten. ## Costs and limits - **Boilerplate.** In verbose languages a value object costs a class, equality, hashing, and serialization support. Records/data classes, or lightweight type aliases in some languages, reduce this a lot. - **Boundary friction.** Serialization, ORM mapping, and API contracts need converters. The usual answer is to keep primitives in the transport/persistence layer and convert once at the boundary. - **Performance.** Wrapping a hot-loop scalar in an object can matter in extreme cases; value/inline types or plain primitives in the inner loop are legitimate escapes. - **Not everything deserves a type.** A genuinely opaque free-text note is fine as a string. The test is whether the value has rules, invariants, or behaviour of its own.
- When is Primitive Obsession the right trade-off to accept?At transport and persistence boundaries (DTOs, wire formats, database columns) where primitives are the contract; in extremely hot numeric loops where wrapping costs measurable performance; and for genuinely opaque values with no rules, such as a free-text comment. The discipline is to convert once at the boundary and keep the domain typed inside.
- Why is representing money as a floating-point number specifically dangerous?Binary floating point cannot represent most decimal fractions exactly, so repeated addition and rounding drift; it also carries no currency, so amounts in different currencies can be summed silently. The standard remedy is a Money value object over a decimal or integer-minor-unit amount, with the currency inside and arithmetic that rejects mixed-currency operations.
Data Clumps are three ingredients you always carry loose in your hands; Primitive Obsession is labelling every jar in the kitchen "powder". Both are fixed by putting things in a named container.
saying these in an interview costs you the question
- Treating any use of string or int as Primitive Obsession — the smell is about *domain concepts*, not about primitives existing
- Introducing a wrapper type but leaving validation outside it, so invalid instances are still constructible
- Bundling parameters that merely co-occur once into a "parameter object" that has no coherent meaning
- Assuming a value object must be mutable-with-setters; value objects are normally immutable and compared by value
- Claiming Data Clumps and Primitive Obsession are unrelated, rather than two shapes of the same missing concept