Skip to content

ALL-DESI1001 · primitive_obsession

Classify whether generic values hide repeated domain rules.

This is a contextual rule for all languages. Read its implementation.

A primitive is a generic value such as str, int, or dict. Using one is normal. The Fowler smell appears only when repeated validation, units, legal states, or operations give that value stable domain meaning. One value with repeated rules suggests a value object. Several values that change state together suggest a domain model. An existing wrapper is useful only when it centralizes those rules. The final category is selected by the explicit decision table, not by the judgment backend.

Each finding names one independently assessed predicate and the exact retained claims behind it, so a reader can check the deterministic result against evidence rather than against the model. Those claims cite repeated validation sites, operations, parameter groups, state transitions, call boundaries, and any existing wrapper. A proposed class is not evidence that an existing abstraction adds knowledge. The repair is always a choice here, because a judgment nobody can reproduce is not an edit.

Local counters, transient parsing values, stable wire formats, and framework-required scalar fields can remain generic when their domain rules are not duplicated. A required generic form is read before any existing wrapper is weighed, so building a domain type out of the raw values a boundary hands over is appropriate rather than one model too many.

.. rubric:: Bad example

Validation for the same monetary concept is repeated at several boundaries.

.. code-block:: python

def charge(amount_minor. int, currency. str) -> None. if amount_minor < 0 or currency not in SUPPORTED_CURRENCIES. raise ValueError(“invalid money”)

.. rubric:: Good example

One immutable value owns the invariant and reusable operation.

.. code-block:: python

class Money(FrozenModel): amount_minor. int currency. Currency

def add(self, other. Money) -> Money.
if self.currency is not other.currency.
raise ValueError("currency mismatch")
return Money(
amount_minor=self.amount_minor + other.amount_minor,
currency=self.currency,
)

A loop counter that never crosses its local algorithm remains an int.

  • Cites “Refactoring”, Primitive Obsession
  • Cites “Domain-Driven Design”, Value Objects
  • Cites “Refactoring Guru”, primitive obsession smell