Skip to content

Primitive Obsession

The code smell of representing domain concepts that carry structural commitments — units, valid ranges, well-formedness, identity — as bare primitives like int or string, so the type system cannot enforce the distinctions and they survive only as validation duplicated at every callsite.

Core Idea

Primitive obsession is the software-design code smell in which a codebase represents domain concepts that carry rich structural commitments — units, valid ranges, well-formedness rules, identity semantics, compositional behaviour — as bare primitives from the language's type system: int, string, float, double. The flattening discards the domain's distinctions from the representation, so the compiler, static analyser, and reader can no longer distinguish a money amount from a temperature reading from a patient identifier; the distinctions exist only as scattered ad-hoc validation, conversion logic, and arithmetic duplicated at every callsite.

The mechanism by which a local choice becomes a systemic liability is accumulation: any single string field representing an email address seems harmless, but every callsite that reads or writes that field must independently re-implement the same well-formedness check, and each re-implementation is a fresh opportunity for inconsistency. The coupling is pervasive but invisible — nothing in the type signature links the scattered sites. Martin Fowler named and catalogued the smell in Refactoring (1999, 2018), prescribing specific countermoves: Replace Data Value with Object introduces a domain type whose constructor enforces well-formedness once at the system boundary; Replace Type Code with Class gives type-level discrimination to values the domain treats as categorically distinct. The complementary discipline — parse-don't-validate, articulated by Alexis King — converts raw primitives into domain types at system entry points and forbids raw primitives beyond them, making illegal states structurally unrepresentable rather than conventionally prohibited. The gain from this conversion is not merely local cleanliness: the type system itself becomes the enforcement mechanism, catching parameter swaps, unit confusion, and ill-formed values at compile time rather than at runtime or in production.

Structural Signature

Sig role-phrases:

  • the structured domain concept — a thing carrying rich commitments (units, valid ranges, well-formedness rules, identity semantics, compositional behaviour): money, email address, duration, patient identifier
  • the primitive representation — the choice to encode it as a bare int, string, float, or double from the language's type system
  • the flattening — the discarding of the domain's distinctions from the representation, so compiler, static analyser, and reader can no longer tell one meaning from another
  • the accumulation — each individual primitive field seeming harmless while every callsite independently re-implements the same check, each re-implementation a fresh chance for inconsistency
  • the dispersed handling — validation, conversion, and arithmetic scattered and duplicated across callsites, the only place the domain's invariants survive
  • the lost compiler signal — no type-level link between the scattered sites, so parameter swaps, unit confusion, and ill-formed values pass unchecked
  • the generic-vs-domain test — whether a primitive stands for a genuinely generic quantity (fine) or a domain value merely representable as a primitive (the smell), decided by whether the flattening carries a summed, non-local cost
  • the type-layer remedy — Replace Data Value with Object, Replace Type Code with Class, parse-don't-validate at the boundary — moving enforcement off the callsites into the type system once, making illegal states structurally unrepresentable

What It Is Not

  • Not "primitives are bad." A primitive standing for a genuinely generic quantity is fine and warrants nothing; the smell is a primitive standing for a domain value that merely happens to be representable as a primitive. The test that separates the two is whether the type system's ignorance of the distinction carries a summed, non-local cost — not the mere presence of an int or string.
  • Not a local lapse to patch at the callsite. The scattered duplicated validation is one defect, not many, and the natural reflex — add one more well-formedness check where the bug surfaced — proliferates the very pattern that is the problem. The corrective lives at the type layer (introduce the value object, parse at the boundary), absorbing the dispersed handling once.
  • Not abstraction itself. Abstraction is the substrate-neutral move of suppressing detail to expose structure; primitive obsession is the software-specific failure to abstract where the domain structure warrants it. The smell is what abstraction-applied-at-the-representation-layer looks like in its absence, not the prime.
  • Not coupling. Coupling is a property of how modules depend on one another; primitive obsession is one cause of high coupling — scattered validation creates non-local dependence on the raw representation — but the two are not the same diagnosis, and the fix targets the missing type, not the dependency graph directly.
  • Not the magic-number smell. Magic number names unnamed constants embedded in logic; primitive obsession is about representation choice — using a built-in primitive where a domain type is warranted — and ranges over types, not just literal constants. Naming the constant does not introduce the type the domain needs.
  • Not the god class or lazy class. Those Fowler siblings name an oversized or under-populated abstraction; primitive obsession names the absence of an abstraction where one is warranted. All three are code smells, but only this one is the missing-type defect.

Scope of Application

Primitive obsession lives within software design and refactoring practice — wherever a language has a type system that can be made to encode the domain's distinctions and a compiler that then enforces them, so a domain concept can be flattened into a bare int/string/float; its reach is within that domain, carrying across paradigms because the mechanism needs only an encodable type system. The non-software gestures (an organization whose roles all collapse to "person") are analogy carried by the parent abstraction prime, not by this refactoring vocabulary.

  • Object-oriented refactoring and DDD — the home of Fowler's catalogue: Replace Data Value with Object and Replace Type Code with Class, with value objects and domain primitives as the domain-driven-design form.
  • Functional / type-driven design — Haskell newtypes, TypeScript branded types, phantom-type tags, and sum types that encode the domain's constraint structure and make illegal states unrepresentable.
  • Strongly-typed embedded and scientific systems — compile-time units-of-measure algebra (F#, Ada SPARK, Boost.Units) lifting primitive quantities into unit-checked types, catching unit-confusion at compile time.
  • Modern multi-paradigm languages — Rust tuple-struct newtypes, Scala/Kotlin value classes, and Swift's type wrappers, where the community treats primitive obsession as a named anti-pattern with named countermeasures.
  • Code-quality review and the code-smell family — review and static-analysis practice flagging the smell alongside its Fowler siblings (god class, lazy class, data clumps, feature envy).

Clarity

Naming primitive obsession separates the symptom a maintainer keeps tripping over — duplicated validation, conversion logic copied across modules, parameter-swap bugs that the compiler waves through — from its single cause: the absence of a domain type to carry the constraint. Without the name, each instance looks like an isolated lapse to be patched where it surfaced, and the natural reflex is to add one more validation check at the offending callsite — which proliferates the very pattern that is the problem. With the name, the engineer sees that the scattered handling is one thing, not many, and that the corrective lives at the type layer (introduce the value object, parse at the boundary), not at the callsite.

It also makes a deceptively reasonable local decision auditable. "Of course an email address is a string — what else would it be?" is unanswerable case by case; the smell-name supplies the answer by reframing the question from "is this field's type wrong here?" to "what is the summed, non-local cost of the type system not knowing this string is an email?" That shift sharpens a distinction the bare type signature erases — generic primitive versus domain value that merely happens to be representable as a primitive — and lets a reviewer ask the sharper question: which of these primitives is load-bearing domain structure flattened into int or string, and where would a single domain type let the compiler enforce what convention is currently failing to?

Manages Complexity

A maintainer surveying a troubled codebase confronts a scatter of seemingly unrelated complaints: validation logic duplicated across three modules with subtly different rules, a parameter-swap bug the compiler waved through, milliseconds-versus-microseconds confusion that caused an outage, a data clump of latitude/longitude that keeps travelling together, magic strings sprinkled through the boundary code, equality that behaves differently depending on which module wrote it. Tracked individually, each is its own ticket with its own local patch, and the codebase presents an open-ended list of defects with no common handle. Primitive obsession collapses that list to a single diagnostic keyed off one observable: is a domain concept that carries structural commitments — units, valid ranges, well-formedness, identity semantics, compositional behaviour — being represented as a bare int, string, float, or double? Wherever the answer is yes, the whole co-occurring syndrome is predicted, because all of it flows from the one cause — the type system not knowing what the value means, so the distinctions survive only as convention re-implemented at each callsite.

The compression is twofold. First, on the diagnostic side, the analyst stops enumerating symptoms and tracks a single binary per field — primitive-where-a-domain-type-is-warranted, or not — and reads the expected cluster of pathologies off it rather than re-deriving each module's troubles from scratch. The branch is sharp: a primitive standing for a generic quantity is fine and warrants nothing; a primitive standing for a domain value that merely happens to be representable as a primitive is the smell, and the test that separates them is whether the type system's ignorance of the distinction carries a summed, non-local cost. Second, on the corrective side, the diagnosis collapses an unbounded search for fixes to a small fixed toolbox — Replace Data Value with Object, Replace Type Code with Class, parse-don't-validate at the boundary — each of which moves enforcement from the scattered callsites into the type layer once. So the engineer reasons from one parameter (does a domain type exist to carry this constraint?) to both the diagnosis and the remedy, instead of treating every duplicated check, swap bug, and unit confusion as a separate problem to be understood and solved on its own terms.

Abstract Reasoning

Primitive obsession licenses design-reasoning moves that all run from a single observable — a domain concept carrying structural commitments represented as a bare int, string, float, or double — to a predicted syndrome and a typed remedy. The most distinctive is a diagnostic move that collapses a scatter of symptoms to one cause. Observing duplicated validation across modules with subtly different rules, a parameter-swap bug the compiler waved through, a milliseconds-versus-microseconds confusion that caused an outage, a latitude/longitude data clump that keeps travelling together, magic strings through the boundary code, the analyst infers a single underlying defect — the type system does not know what the value means, so the domain's distinctions survive only as convention re-implemented at each callsite — and predicts the rest of the cluster from the one cause rather than re-deriving each module's troubles. The reasoning is one binary per field: is a domain concept with structural commitments represented as a bare primitive, or not? Where yes, the whole co-occurring syndrome is predicted to follow.

A boundary-drawing move separates the genuine smell from the harmless case, and it is load-bearing because it decides whether to act at all. The analyst partitions a primitive standing for a generic quantity (fine, warrants nothing) from a primitive standing for a domain value that merely happens to be representable as a primitive (the smell), and applies a sharp test to tell them apart: whether the type system's ignorance of the distinction carries a summed, non-local cost across the codebase. This reframes the otherwise-unanswerable local question — "of course an email is a string; what else would it be?" — into the structural one, "what is the summed cost of the type system not knowing this string is an email?" The analyst reasons over the whole codebase, not the single callsite, because the cost the test measures is precisely the accumulation of duplicated handling that no individual field exhibits.

The interventionist move reasons forward from the diagnosed field to a small fixed toolbox and predicts where enforcement moves. Because the corrective lives at the type layer, not the callsite, the analyst applies Replace Data Value with Object (a domain type whose constructor enforces well-formedness once at the boundary), Replace Type Code with Class (type-level discrimination for categorically distinct values), or parse-don't-validate at system entry points (convert raw primitives into domain types at the edges and forbid raw primitives beyond them) — and explicitly rejects the natural reflex of adding one more validation check at the offending callsite, reasoning that it proliferates the very pattern that is the problem. The signature prediction of this move is a shift in when and how errors are caught: once the domain type carries the constraint, the type system itself becomes the enforcement mechanism, so parameter swaps, unit confusion, and ill-formed values are caught at compile time rather than at runtime or in production — and the analyst reasons toward making illegal states structurally unrepresentable rather than conventionally prohibited. The recurring habit the concept installs is to read each load-bearing primitive and ask whether it is domain structure flattened into int or string, whether the flattening's cost is non-local, and where a single domain type would let the compiler enforce what convention is currently failing to.

Knowledge Transfer

Within software design the concept transfers as mechanism, and the transfer across the field's paradigms is well-established rather than approximate. The same diagnostic (a domain concept carrying structural commitments flattened into a bare primitive), the same load-bearing test (does the type system's ignorance of the distinction carry a summed, non-local cost?), and the same corrective stance (move enforcement off the callsites into the type layer; parse-don't-validate at the edges; make illegal states unrepresentable) carry without translation from object-oriented refactoring (Fowler's Replace Data Value with Object, Replace Type Code with Class; value objects and domain primitives in domain-driven design) to functional type-driven design (Haskell newtypes, TypeScript branded types, phantom-type tags, sum types) to strongly-typed embedded systems (compile-time units-of-measure algebra in F#, Ada SPARK, Boost.Units) to the Rust, Scala, Kotlin, and Swift idioms (tuple-struct newtypes, value classes). The vocabulary does not strain across these because they share the one feature the mechanism needs: a type system that can be made to encode the domain's distinctions and a compiler that then enforces them.

Beyond software the honest account is twofold. The named smell — "primitive obsession," with its Fowler refactorings, value objects, parse-don't-validate boundary discipline, and reliance on compile-time enforcement — does not transfer non-metaphorically: the cross-domain gestures one might reach for (an organization whose roles all collapse to "person," a legal code whose categories all flatten to "agreement," a curriculum that erases a subject's distinctions) borrow the shape and rename the components but drop the entire load-bearing apparatus (there is no type system, no compiler signal, no refactoring catalogue, no system boundary to parse at). Those are analogy, case (A), and should be marked so. But the principle the smell instantiates is a genuine shared abstract mechanism — case (B): represent the domain's structural commitments in the medium of the system rather than flattening them and re-introducing them ad hoc at the points of use. That principle really does recur across substrates, and it carries a real lesson wherever a representation can either encode or discard the distinctions its domain makes. Crucially, that principle is already in the catalog as a prime — it is abstraction applied at the representation layer, with the standard corollary that an abstraction should preserve the distinctions the domain warrants. So the honest move is that the cross-domain reach belongs to the parent abstraction, not to "primitive obsession," whose specific refactoring vocabulary and compiler-enforcement payoff are software-design furniture that does not survive extraction. Primitive obsession is one code-smell-grain recognition of this shape inside software (sibling to god class, lazy class, data clumps), and it does not, as named, float free of the language type system (see Structural Core vs. Domain Accent).

Examples

Canonical

The archetypal instance is a money-transfer function whose parameters are all bare primitives: transfer(long fromAccount, long toAccount, double amount). Every value is domain-loaded — account identifiers are opaque tokens that must never be arithmetic operands, and amount carries a currency and a required non-negativity and rounding discipline — yet the type system sees only two longs and a double. A caller who writes transfer(to, from, amount) with the accounts swapped compiles cleanly and ships the bug to production; representing dollars as a binary double invites rounding errors (0.1 + 0.2 ≠ 0.3). The parse-don't-validate fix introduces AccountId and Money value types whose constructors enforce well-formedness once, so the swap becomes a compile error (an AccountId is not interchangeable with another positionally without naming) and money arithmetic lives in one audited place rather than being re-checked at each callsite.

Mapped back: Account IDs and monetary amounts are the structured domain concepts; encoding them as long and double is the primitive representation whose flattening produces the lost compiler signal — the swapped-argument bug the compiler waves through. Introducing AccountId/Money value objects is the type-layer remedy, moving enforcement off the callsites into the type system once.

Applied / In Practice

NASA's Mars Climate Orbiter, lost on September 23, 1999, is the most expensive real-world instance of the underlying defect. The spacecraft's trajectory software combined output from two teams: Lockheed Martin's ground software produced thruster impulse values in pound-force-seconds (imperial), while NASA JPL's navigation software consumed them expecting newton-seconds (metric). Both quantities were represented as bare numbers with no unit carried in the type, so nothing in the interface flagged the mismatch. The numeric values passed silently between systems; the ~4.45x discrepancy accumulated over the cruise and drove the orbiter too low into the Martian atmosphere, where it was destroyed. A units-of-measure type system — where a value is typed Newton·s versus lbf·s and the compiler refuses to combine them without explicit conversion — would have caught the mismatch at build time.

Mapped back: The impulse quantity is the structured domain concept carrying a unit commitment; representing it as a plain number is the primitive representation and flattening. The absence of a unit in the type is the lost compiler signal that let incompatible values combine — precisely the summed, non-local cost the generic-vs-domain test measures, remediable by lifting quantities into unit-checked types.

Structural Tensions

T1: Harmless generic primitive versus flattened domain value (the boundary that licenses action). The concept insists that not every int or string is a smell — a primitive standing for a genuinely generic quantity warrants nothing, and only a domain value that merely happens to be representable as a primitive qualifies. But the two are indistinguishable at a single callsite: "of course an email is a string; what else would it be?" is unanswerable field by field. The discriminating test is non-local — whether the type system's ignorance of the distinction carries a summed cost across the codebase — which means the boundary can only be drawn by reasoning over the whole system, not the field in front of you. Draw it too eagerly and every primitive becomes a value-object candidate; draw it too grudgingly and load-bearing structure stays flattened. Diagnostic: Does the type system's not knowing this value's meaning cost anything beyond this one callsite?

T2: The reflexive callsite patch versus the type-layer remedy (which lever proliferates and which absorbs). When a swap bug or an ill-formed value surfaces, the natural corrective is to add one more well-formedness check where it broke. The concept marks this as precisely wrong: each new callsite validation is another re-implementation of the invariant, a fresh chance for inconsistency, and it proliferates the very pattern that is the problem. The reliable fix lives one layer up — a value object whose constructor enforces the rule once, parse-don't-validate at the boundary — absorbing the dispersed handling rather than adding to it. The tension is that the intuitive lever operates on the corrupted layer (the callsites) while the only durable one bypasses them, and the local patch always looks cheaper in the moment than restructuring the type. Diagnostic: Does the proposed fix add enforcement at a callsite, or move it into the type where the constraint lives once?

T3: Upfront type ceremony versus deferred, summed production cost (why the smell survives). A bare string is cheap to write today; an AccountId or Money type demands a constructor, invariants, and conversions before the first line of business logic runs. That upfront cost is local, visible, and paid now, while the cost the smell imposes — duplicated validation, swap bugs, unit confusion — is non-local, invisible in any one signature, and paid later and in aggregate. The very asymmetry that makes each individual flattening "seem harmless" is what lets the syndrome accumulate: no field exhibits the summed cost, so no field ever seems to justify the ceremony. The tension is real and not dissolved by fiat — over-typing a genuinely generic quantity is waste, but under-typing a domain value defers a larger bill. Diagnostic: Is the type's construction cost being weighed against this one field, or against the summed non-local cost of its absence?

T4: Parse at the boundary versus primitives within (where the entry line falls). Parse-don't-validate prescribes converting raw primitives into domain types at system entry points and forbidding raw primitives beyond them — but this works only if the boundary is drawn where the raw input actually enters, and locating it is itself a judgment. Place the parse too deep and raw strings and ints circulate through interior code, re-validated ad hoc, exactly the dispersed handling the discipline was meant to end; place it too shallow, at every function, and the interior drowns in wrapping and unwrapping. The Mars Climate Orbiter failure was a boundary that did not parse: two subsystems exchanged bare numbers across the very interface where units should have been lifted into the type. The tension is that the boundary is the whole leverage of the fix and also the hardest thing to site correctly. Diagnostic: At the point where this raw value enters, is it converted into a domain type, or does it travel inward still primitive?

T5: The missing-type smell versus its look-alike neighbors (bounding what the diagnosis names). Primitive obsession is repeatedly conflated with the pathologies it sits among. It is not "primitives are bad" — a generic quantity as an int is fine. It is not coupling, though it causes coupling by creating non-local dependence on a raw representation. It is not the magic-number smell, which names unnamed constants rather than representation choice. It is not the god or lazy class, its Fowler siblings, which name an oversized or under-populated abstraction where this names the outright absence of one. And it is not abstraction itself — it is what abstraction-at-the-representation-layer looks like in its absence. The tension is that the concept is flanked by relatives it shares symptoms with, and folding any of them in mislocates the fix, aiming at the dependency graph or a literal constant rather than the missing type. Diagnostic: Is the defect the absence of a warranted domain type, or a distinct pathology (a constant, a dependency, an oversized class) that merely co-occurs with it?

T6: Autonomy versus reduction (its own named smell or the software instance of abstraction). "Primitive obsession" is a canonically named code smell with proprietary furniture — Fowler's Replace Data Value with Object and Replace Type Code with Class, value objects, parse-don't-validate, compile-time enforcement — and inside software it transfers intact across object-oriented, functional, embedded, and multi-paradigm type systems because all it needs is an encodable type system and a compiler. But beyond software the named smell does not travel: an organization whose roles all collapse to "person" borrows the shape and drops the apparatus — no compiler, no refactoring catalogue, no boundary to parse at. What genuinely carries is the parent it instantiates — abstraction applied at the representation layer, with the corollary that a representation should preserve the distinctions its domain warrants. The tension is between a standalone software smell that earns its own catalogue entry and the recognition that its cross-domain cargo already belongs to abstraction. Diagnostic: Resolve toward the parent (abstraction) when asking what travels outside software; toward primitive obsession when diagnosing a flattened domain type at a callsite.

Structural–Framed Character

Primitive obsession sits at the framed-leaning end of the spectrum — well short of the pure-verdict framed pole that a label like ad hominem occupies, but decisively more framed than a nature-running mechanism like isostasy, because it is constituted by an engineering practice and carries a corrective judgment. On evaluative_weight it points framed: "smell" is not a neutral description but a flag that something ought to be fixed — the concept exists to license a refactoring, and to call a field primitive obsession is to say the representation is defective and should be lifted into a type. The verdict is softened by the entry's own boundary (T1: a generic primitive "warrants nothing"; the diagnosis is a summed-cost test, not a blanket condemnation), so the evaluative charge is diagnostic rather than moral — but it is still a normative pull toward action, not the value-free stance of "a column rising toward balance." On human_practice_bound it points strongly framed: strip away the human practice of software engineering — the type system, the compiler, the refactoring catalogue, the system boundary to parse at — and there is nothing left to be obsessed about; unlike a lithosphere that rebounds observer-free, primitive obsession does not exist without programmers and the artifacts they build. On institutional_origin likewise framed: the concept is furniture of a specific tradition — Fowler's Refactoring, its named countermoves, King's parse-don't-validate discipline, the code-smell family (god class, lazy class, data clumps) — an artifact of a design culture, not a fact nature already performs.

The remaining two criteria pull the other way and are what keep it from the framed pole. On vocab_travels it is pinned: value object, newtype, phantom type, compile-time units algebra, parse-at-the-boundary, lost compiler signal are irreducibly type-system vocabulary and lose their referents off the software substrate. And on import_vs_recognize the transfer is bimodal in the now-familiar way — within software (OO refactoring, functional type-driven design, embedded units algebra, Rust/Scala/Kotlin/Swift idioms) it moves as recognition of the very same mechanism, needing only an encodable type system; beyond software the gestures ("an organization whose roles all collapse to 'person'") are import-by-analogy that rename the parts and drop the whole apparatus.

The portable structural skeleton is not proprietary to this smell: it is abstraction applied at the representation layer — encode the domain's structural distinctions in the medium of the system rather than flattening them and re-introducing them ad hoc at each point of use. That principle genuinely recurs across substrates, which is what tempts a structural reading, but it is exactly what primitive obsession instantiates from its parent prime, not what makes "primitive obsession" itself travel: the cross-domain reach belongs to abstraction, while the smell's distinctive content — the type-layer remedies, the compiler-enforcement payoff, the Fowler catalogue — is software-design furniture that stays home. Its character: a practice-constituted, mildly normative refactoring diagnosis whose portable core is just abstraction-at-the-representation-layer, structural in that borrowed skeleton but framed-leaning in everything that makes it "primitive obsession" in particular.

Structural Core vs. Domain Accent

This section decides why primitive obsession is a domain-specific abstraction and not a prime, and with it why it is domain-specific at all.

What is skeletal (could lift toward a cross-domain prime). Strip the type system away and a thin relational structure survives: a representation either encodes the distinctions its domain warrants or flattens them, and where it flattens them the distinctions do not vanish but reappear, re-implemented ad hoc, at every point of use. Stated abstractly the portable pieces are a domain with real internal distinctions, a medium that can either carry or discard them, and a summed non-local cost that falls on the system when the medium discards them and the points of use must each restore them. That skeleton is genuinely substrate-portable — it is the failure to abstract where structure warrants abstracting — which is exactly why it recurs as the parent prime abstraction (with the corollary that an abstraction should preserve the distinctions its domain makes). But this is the core primitive obsession shares with every representation-choice problem, not what makes it primitive obsession.

What is domain-bound. Almost everything that makes the concept this smell is software-design furniture that does not survive extraction. It presupposes a language type system that can encode the domain's distinctions and a compiler that then enforces them: the int/string/float/double primitives, the lost compiler signal that lets a parameter-swap or unit-confusion bug pass unchecked, the value object and newtype and phantom type, the compile-time units-of-measure algebra, the parse-don't-validate boundary discipline, and Fowler's named countermoves (Replace Data Value with Object, Replace Type Code with Class). These are the worked vocabulary, the instruments, and the empirical cases — the Mars Climate Orbiter's untyped impulse quantity, the swapped AccountId arguments — and all of them are specific to the software substrate. The decisive test: remove the type system and the compiler and there is nothing left to be obsessed about — the cross-domain gestures (an organization whose roles collapse to "person," a legal code whose categories flatten to "agreement") rename the components and drop the entire enforcement apparatus, becoming a looser thing than the named smell.

Why this does not clear the prime bar. A prime's vocabulary travels and its cross-domain transfer is recognition of the same mechanism, not analogy. Primitive obsession's transfer is bimodal. Within software it moves intact — object-oriented refactoring, functional type-driven design, embedded units algebra, and the Rust/Scala/Kotlin/Swift idioms all share an encodable type system, so the same diagnostic, the same summed-cost test, and the same move-enforcement-to-the-type-layer corrective port as recognition of one mechanism. Beyond software it travels only by analogy: the "roles-collapse-to-person" gesture borrows the shape but has no compiler, no refactoring catalogue, and no boundary to parse at. And when the bare structural lesson is needed cross-domain — encode the domain's distinctions in the medium rather than flattening and re-introducing them at each point of use — it is already carried, in more general form, by the parent abstraction (applied at the representation layer). The cross-domain reach belongs to that parent; "primitive obsession," as named, carries the type-layer, compiler-enforcement, Fowler-catalogue baggage that should stay home in software design.

Relationships to Other Abstractions

Local relationship map for Primitive ObsessionParents appear above the current abstraction, mutual partners to the right, and children below. Node labels state whether each abstraction is prime or domain-specific; colors identify relation types.Primitive ObsessionDOMAINDomain-specific abstraction: Code Smell — is a kind ofCode SmellDOMAIN

Current abstraction Primitive Obsession Domain-specific

Parents (1) — more general patterns this builds on

  • Primitive Obsession is a kind of Code Smell Domain-specific

    Primitive Obsession is the code-smell species whose repeated use of bare built-ins provides defeasible evidence that warranted domain distinctions are missing from the type representation.

Hierarchy paths (4) — routes to 4 parentless roots

Not to Be Confused With

  • Stringly-typed programming. The narrower anti-pattern of encoding structured or enumerable values specifically as string — state flags, dates, enums passed as free text. It is a subtype of primitive obsession, not a peer: primitive obsession ranges over all built-in primitives (int, float, double, and identity-bearing tokens) and over the fuller set of domain commitments (units, ranges, arithmetic, compositional behaviour), while stringly-typed names the string-only special case. Tell: is the flattening confined to values crammed into string (stringly-typed), or does it also cover numeric quantities, identifiers, and unit-bearing values encoded as bare numbers (the broader smell)?
  • Magic number / magic string. An unnamed literal constant embedded directly in logic (if status == 3), fixed by naming the constant. Primitive obsession is about the type of a value, not whether a literal is named: giving 3 the name ACTIVE removes the magic number but leaves the status still a bare int the domain warrants a type for. Tell: does introducing a named constant resolve it (magic number), or does the defect persist until a domain type carries the constraint (primitive obsession)?
  • Data clumps. The Fowler sibling in which the same group of primitives keeps travelling together (latitude/longitude, start/end date) and ought to be bundled into one object. It overlaps primitive obsession — the remedy is often the same value object — but data clumps is diagnosed by recurring co-occurrence of several fields, whereas primitive obsession fires on a single domain value flattened into one primitive. Tell: is the smell a repeated cluster of parameters that always appear together (data clumps), or one load-bearing value whose type discards its domain meaning (primitive obsession)?
  • God class / lazy class. Fowler siblings naming an oversized abstraction that does too much (god class) or an under-populated one that earns nothing (lazy class). These name the wrong size of an abstraction that exists; primitive obsession names the outright absence of an abstraction the domain warrants. Tell: is there a class present that is mis-sized (god/lazy), or is there no domain type at all where one is needed (primitive obsession)?
  • Coupling. A property of how modules depend on one another. Primitive obsession is one cause of high coupling — scattered validation creates non-local dependence on a raw representation — but the two are not the same diagnosis, and the fix targets the missing type, not the dependency graph. Tell: are you describing modules' mutual dependence (coupling), or the flattened representation that generates it (primitive obsession)?
  • Abstraction (parent prime). The substrate-neutral move of suppressing detail to expose structure, with the corollary that a representation should preserve the distinctions its domain warrants. Primitive obsession is what abstraction-at-the-representation-layer looks like in its absence, keyed to software's type system; it is the parent's software instance, not a confusable peer. Tell: the parent carries the cross-domain reach (an org whose roles collapse to "person" borrows only the shape); primitive obsession is the type-system-bound failure to abstract, treated more fully in the sections above.

Neighborhood in Abstraction Space

Primitive Obsession sits in a crowded region of the domain-specific corpus (13th percentile for distinctiveness): several abstractions share nearly its structure, so a description that fits it tends to fit its neighbors too.

Family — Surface Form & Underlying Structure (23 abstractions)

Nearest neighbors

Computed from structural-signature embeddings · 2026-07-12