Skip to content

Data Class

Flag an object-oriented class that holds fields with getters and setters but no domain methods as a symptom that its behavior migrated outward into consumers, then resolve the verdict with one role question — should this class enforce its own invariants?

Core Idea

A data class is an object-oriented code smell in which a class holds fields and exposes them via getters and setters but contains no methods that perform meaningful domain operations — the behavior that belongs to the class has migrated outward and lives instead in the classes that consume it. The object-oriented organizing principle being violated is encapsulation: a class should bundle data and the operations on that data together, enforcing its own invariants so that consumers can rely on them without re-deriving them. A data class inverts this: the fields are exposed, the invariants are nowhere enforced, and each consumer independently implements whatever operations it needs on the class's data. The class becomes a passive record container — a struct wearing object syntax.

The structural consequences compound with the number of consumers. A change to the data layout requires finding and updating every consumer that reached directly into the fields; the class itself offers no stable behavioral surface to hide behind. Validation logic that should be centralized in the class's own methods is instead duplicated across consumers, each duplicating the logic slightly differently, so the same data can be in inconsistent states depending on which consumer last touched it. The class accumulates getters and setters but never acquires methods that express domain concepts — a pattern Martin Fowler catalogs as a code smell in Refactoring, with the canonical remedy being Move Method: identify behavior scattered across consumers that properly belongs to the class and move it in, until the class has a behavioral surface commensurate with its data.

The diagnostic question that separates this anti-pattern from legitimate usage is whether the class should be enforcing invariants on its own data. If the answer is yes — if the data represents a domain entity with meaningful constraints, state transitions, or business rules — then the class is anaemic and the behavior should move in. If the answer is no — if the class is an intentional pure-data record with no behavioral logic, meant to be constructed once and read — then it is not an anti-pattern but a deliberate value object, and modern language features exist to signal this intent explicitly: Java record, Kotlin data class, C# record struct, Python @dataclass. These constructs declare immutability or value-equality semantics and remove the getters-and-setters ceremony; the same surface that is a smell in an entity class is a feature in a deliberate value object. The data class smell is therefore not about the presence of fields but about the mismatch between the class's role as a domain entity and its absence of the behavior that role implies.

Structural Signature

Sig role-phrases:

  • the accessor-only class — a class exposing fields via getters and setters but holding no domain methods (a struct wearing object syntax)
  • the violated encapsulation — the expectation that data and the operations maintaining its invariants travel together, here broken
  • the dispersed consumers — the classes that reached the behavior outward, each independently implementing operations that should belong to the class
  • the unenforced invariants — constraints nowhere centralized, so validation is duplicated across consumers and the data drifts into inconsistent states
  • the role question — the one binary the syntax cannot answer: should this class be enforcing invariants on its own data?
  • the anaemic-entity verdict — yes-branch: a domain entity missing its behavior; the smell is a real defect
  • the deliberate-record verdict — no-branch: an intentional pure-data value object; the identical surface is a feature
  • the Move Method remedy — the fix for the anaemic branch: trace dispersed operations back to the data and relocate them in
  • the formalization remedy — the fix for the record branch: declare immutability/value-equality (Java record, @dataclass) to signal intent
  • the consumer-count cost — change cost scales with the number of consumers reaching directly into the fields

What It Is Not

  • Not every accessor-only class. A deliberate value object — fields, getters, no behavior — has a byte-for-byte identical surface and is perfectly correct. The smell is not the shape but the mismatch between a class's role as a domain entity and its missing behavior; condemning every class that lacks methods wrongly flags honest records.
  • Not "the class is too simple." A data class is a symptom whose cause is offstage: the behavior that belongs to it has not vanished from the system, it migrated outward into the consumers, where validation logic is now duplicated and quietly diverging. The defect is dispersed, not local — the thin class is the signal, not the whole problem.
  • Not merely "a class with fields." Holding data is not the offense — every object holds state. The smell is the absence of the operations that maintain that data's invariants, so the class cannot vouch for its own consistency. Reaching for "it has too many fields" mistakes the data for the defect.
  • Not a universal anti-pattern. The verdict is relative to encapsulation, an object-oriented commitment. In functional programming, the relational model, and dataflow systems, separating data from the operations over it is the default and correct arrangement — nothing migrated, nothing is wrong. Calling that separation a data-class smell imports an expectation those paradigms never made.
  • Not fixed by switching to a record type. Java record, Kotlin data class, C# record struct, and Python @dataclass do not make a genuine smell disappear — they declare that the no-behavior case was intended all along. For an anaemic entity the remedy is the opposite: Move Method, pulling the dispersed behavior back in until the class earns its data.

Scope of Application

The data-class smell lives within object-oriented software design, across the subfields that share encapsulation as their explicit organizing principle; its reach is bounded to that paradigm, because the verdict that separating data from operations is a defect only holds where the class was expected to bundle them — outside OO the same separation is the default, and the genuine cross-domain echoes belong to the encapsulation/coupling primes and the separation-of-knowledge-from-action family, not here.

  • Object-oriented design proper — Java/C# beans with only getters and setters and no domain methods; the everyday review target where the smell is named and caught.
  • Layered enterprise architecture — the same condition appears as the "Anaemic Domain Model" critique, where service layers hold the behavior that should sit on the entities.
  • Domain-Driven Design — a central concern, with rich invariant-owning aggregates as the contrasting target the smell warns you have failed to build.
  • Refactoring practice — a catalogued code smell in Fowler's Refactoring, sitting beside its encapsulation-and-coupling siblings (Feature Envy, Inappropriate Intimacy, Shotgun Surgery) and resolved by Move Method.
  • Modern value-object language features — Java record, Kotlin data class, C# record struct, and Python @dataclass are where the no-behavior case is deliberately legitimized, formalizing the same surface as intent rather than as a smell.

Clarity

Naming the data class smell makes a verdict legible that the source code itself hides: two classes with the same syntactic surface — fields, getters and setters, no behavior — can warrant opposite judgments, and the name forces the reviewer to decide which one is in front of them. A deliberate value object and an accidental anaemic entity are indistinguishable by shape; what separates them is a question the syntax cannot answer on its own — should this class be enforcing invariants on its own data? If yes, the class is anaemic and the missing behavior is a defect to be moved in; if no, it is a legitimate record to be formalized as such. Without the concept, a reviewer either flags every accessor-only class (and wrongly condemns honest value objects) or excuses them all (and lets domain logic leak); with it, the smell points to the one diagnostic that resolves the case.

The frame also relocates where the practitioner looks for the problem. A data class is a symptom whose cause is offstage: the behavior that belongs to it is not absent from the system, it has migrated outward into the consumers, which is why the class looks innocently thin while validation logic quietly duplicates and diverges across the codebase. Recognizing the smell reframes "this class is too simple" into the sharper "where did this class's behavior go, and is it now scattered and inconsistent?" — which is exactly the question the Move Method remedy answers, by tracing dispersed operations back to the data they act on. It separates the real failure (the mismatch between an entity's role and its missing behavior) from a non-issue (the mere presence of fields), so that the modern record/data class/@dataclass constructs are read correctly: not as a way to make the smell go away, but as a way to declare that the no-behavior case was intended all along.

Manages Complexity

A large object-oriented codebase presents a reviewer with thousands of classes and an open-ended worry: somewhere in this system, domain behavior has drifted away from the data it acts on, validation is being duplicated and quietly diverging, and a change to one record's layout will ripple through every consumer that reached into it. Tracking that worry directly would mean auditing every class against every consumer to reconstruct where each operation lives and whether the data's invariants are enforced anywhere — an inspection that scales with the cross-product of classes and call sites. The data-class smell compresses that audit to a single recognizable shape: a class that is all fields, getters, and setters and no domain methods. Spotting that shape lets the reviewer stop scanning behavior across the whole system and localize the candidate failure to one class's surface — the smell is the cheap, local signal that the expensive, dispersed condition (scattered behavior, duplicated invariants, change cost that grows with consumer count) may be present.

But the shape alone is ambiguous — a deliberate value object and an anaemic entity are byte-for-byte indistinguishable in syntax — so the second move is what does the real compression: it collapses the verdict to one binary parameter. Should this class be enforcing invariants on its own data? Yes routes to "anaemic — move the behavior in" (Move Method, traced back along the consumers that hold the dispersed operations); no routes to "deliberate record — formalize it" (Java record, @dataclass, value-equality, immutability). Every downstream consequence — whether the duplicated validation is a defect or a non-issue, whether the missing methods are a hole or by design, whether the modern record constructs are a fix or a declaration of original intent — is read off that single answer rather than re-derived per class. So instead of holding the full behavior-to-data map of the codebase in mind, the practitioner tracks two things: a local syntactic shape (accessor-only class) and one role question (entity or record), and reads the refactoring verdict and its severity off their combination. A diffuse, system-wide complexity ("where did all the behavior go, and is the data ever vouched for?") collapses to a per-class smell plus a one-bit branch.

Abstract Reasoning

The data-class smell licenses a compact reasoning routine for OO code review, built on a local syntactic signature, a single role-question that resolves an irreducible ambiguity, and a remedy keyed to the answer.

Diagnostic — read a thin class as the symptom of behavior that migrated offstage. The defining move treats an accessor-only class not as a self-contained simplicity but as a symptom whose cause is elsewhere. Spotting the shape — fields, getters and setters, no domain methods — the reviewer reasons FROM the class's innocent thinness TO a dispersed condition in the consumers: the behavior that belongs to the class has migrated outward, so validation logic is being duplicated across consumers and quietly diverging, and the same data can sit in inconsistent states depending on which consumer last touched it. The smell is the cheap, local signal that an expensive, distributed defect may be present, so the reviewer stops auditing behavior across the whole system and localizes the candidate failure to one class's surface. The inference reframes the lazy observation "this class is too simple" into the sharp "where did this class's behavior go, and is it now scattered and inconsistent?"

Boundary-drawing — resolve a syntactically-undecidable verdict with one role question. The characteristic move confronts an ambiguity the source code cannot settle: a deliberate value object and an accidental anaemic entity have byte-for-byte identical surfaces, so shape alone yields no verdict. The reviewer draws the line with a single binary the syntax cannot answer — should this class be enforcing invariants on its own data? Yes means the class is anaemic, a domain entity missing the behavior its role implies, and the smell is a real defect; no means it is an intentional pure-data record, and the identical surface is a feature, not a smell. This boundary is what stops the reviewer from the two failure modes the unaided eye falls into — condemning every accessor-only class (wrongly flagging honest value objects) or excusing them all (letting domain logic leak) — and it correctly reads the modern record constructs (Java record, Kotlin data class, C# record struct, Python @dataclass) as a way to declare the no-behavior case was intended, not as a way to make a genuine smell disappear.

Interventionist — route to a remedy by the role answer, and trace it back along the consumers. The two branches dictate two different actions, each predicted to fix the corresponding condition. The "anaemic entity" branch routes to Move Method: the reviewer reasons backward from the dispersed operations in the consumers to the data they act on, relocating that behavior into the class until it has a behavioral surface commensurate with its data — and predicts that centralizing the operations collapses the duplicated, diverging validation into one place that enforces the invariant. The "deliberate record" branch routes to formalization: declare immutability and value-equality with a record construct and remove the getters-and-setters ceremony, predicting that this signals intent so future reviewers stop mistaking the record for an anaemic entity. The reviewer selects the remedy by the one role answer rather than reasoning about each scattered call site from scratch.

Predictive — read change-cost and severity off the consumer count and the role answer. From the smell plus its branch the reviewer predicts the maintenance consequences before they bite. For an anaemic entity, change cost is predicted to scale with the number of consumers: because each reaches directly into the fields and the class offers no stable behavioral surface to hide behind, a change to the data layout forces finding and updating every consumer in lockstep, and the duplicated validation is predicted to drift further with each independent edit. For a deliberate record, the same surface is predicted to carry none of these costs, because no behavior was ever supposed to live there. So the reviewer reads both the refactoring verdict and its severity off two cheap inputs — the local accessor-only shape and the one-bit role question — rather than reconstructing the full behavior-to-data map of the codebase.

Knowledge Transfer

Within object-oriented software the data-class smell transfers as mechanism. The diagnostic (spot the accessor-only shape, then ask the one role question — should this class enforce invariants on its own data?), the verdict branch (anaemic entity versus deliberate value object), and the remedies (Move Method to pull dispersed behavior back in; or formalize with Java record, Kotlin data class, C# record struct, Python @dataclass) carry intact across the OO subfields that share encapsulation as their explicit organizing principle. They apply unchanged to enterprise-layered architectures (where the same condition is named the "anaemic domain model"), to Domain-Driven Design (where rich invariant-owning objects are the contrasting target), and to refactoring practice generally, sitting alongside its sibling smells — Feature Envy, Inappropriate Intimacy, Shotgun Surgery — that share the same encapsulation-and-coupling substrate. The transfer is mechanical rather than analogical here because all of these run on the same paradigmatic commitment: a class ought to bundle data with the operations that maintain its invariants, so the absence of that bundling is a real, locatable defect with a known cause (behavior migrated outward) and a known fix.

Beyond the OO paradigm the concept does not transfer even as a failure — and saying so honestly is the whole point. The data-class smell is a verdict relative to a specific commitment, and where that commitment is absent the "anti-pattern" simply is not one. In functional programming (data types and the functions over them are deliberately separate), in the relational model (tables and queries are separate by design), and in dataflow or pipe-and-filter systems, separating data from operations is the default and correct arrangement, not a smell; there is no behavior that "should have" lived with the data, so nothing has migrated and nothing is wrong. This makes the cross-domain situation an instructive instance of case (B): what genuinely recurs across substrates is not "data class" but the parent structures the smell instantiates — the absence of encapsulation (state and the operations that guard it are pulled apart) and the resulting coupling (consumers depend on internal structure, so a layout change ripples through all of them), typically with low cohesion. Those primes travel; the OO verdict that the separation is a defect does not, because the verdict depends on a paradigm-local expectation. The cross-domain echoes the source gestures at — organizations separating knowledge from decision rights, law separating fact from procedure, pedagogy separating content from feedback — are real and worth studying, but they belong to a separate separation-of-knowledge-from-action family, not to "data class"; calling any of them a data-class smell borrows the shape while importing an encapsulation expectation those domains never made, which is analogy, and a misleading one. The honest lesson to carry across domains is the parent (encapsulation/coupling/cohesion, or the knowledge-action separation family where that is the real subject), not the named code smell, whose load-bearing content — that the separation is wrong — is OO-paradigm furniture that does not and should not travel. (See Structural Core vs. Domain Accent.)

Examples

Canonical

Consider a DateRange class in a booking system that holds two fields, start and end, exposes them through getters and setters, and does nothing else — a struct wearing object syntax. The operations that belong to it live in its consumers: the reservation module checks start.isBefore(end) before saving, the billing module recomputes the same check, and a reporting job computes end - start for a duration — each reaching directly into the fields, each re-deriving the invariant that a range's start must precede its end, and each doing it slightly differently. Fowler's Refactoring catalogs exactly this as the Data Class smell. The remedy is Move Method: relocate contains(), overlaps(), durationInDays(), and the start-before-end validation into DateRange itself, until the class owns and enforces the invariant its consumers were separately re-implementing.

Mapped back: DateRange is the accessor-only class; bundling data with the operations that guard it is the violated encapsulation. The reservation, billing, and reporting modules are the dispersed consumers holding the unenforced invariants (start-before-end, checked three divergent ways). The role question answers yes — it should enforce its own constraint — so it is anaemic, and the Move Method remedy pulls the behaviour home.

Applied / In Practice

In a large fraction of enterprise Java and C# systems the smell appears at architectural scale as what Martin Fowler named the "Anemic Domain Model" (2003). Persistence entities — a JPA Account, an Order — are generated as beans with fields, getters, and setters and no behaviour, while the business rules (can this account be debited? is this order still cancellable?) live in a separate layer of @Service classes that reach into the entities' fields. The invariants are centralized nowhere, so the same rule is duplicated across services and drifts. The reviewer applies the one role question — should Account enforce its own overdraft invariant? — answers yes, and the Domain-Driven Design remedy (Evans) is precisely Move Method at scale: push behaviour back onto the entities until they are rich objects guarding their own state, leaving services to coordinate rather than compute.

Mapped back: The JPA entity is the accessor-only class; the @Service layer is the dispersed consumers into which behaviour migrated, leaving the unenforced invariants (overdraft, cancellability) duplicated and drifting. The role question resolves to the anaemic-entity verdict, and the DDD fix is the Move Method remedy applied across the architecture.

Structural Tensions

T1: Identical surface versus opposite verdict (the syntax cannot decide). A deliberate value object and an accidental anaemic entity are byte-for-byte indistinguishable — fields, getters, setters, no behavior — yet warrant opposite judgments. The smell is therefore not the shape but the mismatch between a class's role and its missing behavior, which means no purely syntactic detector can settle the case: it takes a role question the code cannot answer (should this class enforce its own invariants?). This cuts both ways. The reviewer who flags every accessor-only class wrongly condemns honest records; the one who excuses them all lets domain logic leak. The concept's precision comes exactly from refusing to read the verdict off shape, which is also what stops it from being mechanized. Diagnostic: Is the class a domain entity that should own invariants (anaemic, a defect) or an intentional pure-data record (a feature) — a question the syntax alone cannot answer?

T2: Local symptom versus dispersed cause (spotted here, fixed there). The thin class is a cheap, local signal — a shape a reviewer can spot on one screen. But its cause is offstage and distributed: the behavior that belongs to the class migrated outward into consumers, where validation is duplicated and quietly diverging. So the smell's value is that it localizes a candidate failure, and its trap is that the failure it points to is not where it is seen. Fixing it means tracing dispersed operations across an unknown number of call sites back to the data — an inspection whose cost is not visible in the thin class that triggered it. The signal is local; the defect and its remedy are not. Diagnostic: Is the concern the class's own thinness, or the scattered, diverging behavior in its consumers that the thinness only signals?

T3: Move Method versus record formalization (opposite remedies for one surface). The identical accessor-only surface routes to two contradictory fixes depending on the role answer: for an anaemic entity, pull behavior in (Move Method) until the class owns its invariants; for a deliberate record, declare intent with a record/data class/@dataclass and remove the ceremony. The danger is reaching for the record type to make a genuine smell disappear — formalizing an entity that should have been enriched, thereby blessing the leak instead of repairing it. The two remedies point in opposite directions (add behavior versus certify its absence), and choosing between them is entirely downstream of a role judgment the language feature itself does not make. Diagnostic: Should this class acquire the behavior scattered in its consumers (Move Method), or was no behavior ever meant to live here (formalize as a record)?

T4: Shape versus severity (the smell's urgency scales with consumers). Spotting the accessor-only shape flags a candidate defect, but says nothing on its own about how bad it is. The change cost of an anaemic entity scales with the number of consumers reaching directly into its fields — a data class with a single consumer is nearly harmless, while one with dozens turns every layout change into lockstep edits and lets duplicated validation drift widely. So the same shape can be a shrug or an emergency, and reading urgency off the shape alone over- or under-prioritizes the refactor. The smell locates the problem; the consumer count and the role answer, not the shape, set its severity. Diagnostic: How many consumers reach directly into these fields, and does the class own an invariant they each re-derive — or is the shape locally contained and low-cost?

T5: Autonomy versus reduction (an OO code smell or the encapsulation-coupling parents). "Data class" is a real, locatable defect relative to a specific paradigmatic commitment — that a class ought to bundle data with the operations guarding its invariants. Within object-oriented software the smell transfers as mechanism, with its role question, its verdict branch, and its remedies intact. But the verdict is paradigm-local: in functional programming, the relational model, and dataflow systems, separating data from operations is the default and correct arrangement, so nothing migrated and nothing is wrong. What genuinely recurs across substrates is not "data class" but the parents it instantiates — a break in encapsulation, the resulting coupling, low cohesion — or, for the organizational echoes, the separation-of-knowledge-from-action family. The tension is between a named smell whose load-bearing content (the separation is wrong) is OO furniture, and the substrate-general primes that actually travel. Diagnostic: Resolve toward the parents (encapsulation, coupling, cohesion) when reasoning outside OO; toward the named smell when the paradigm-local expectation that data and behavior belong together actually holds.

Structural–Framed Character

The data-class smell sits at the framed-leaning position — a paradigm-relative defect verdict, constituted by object-oriented design practice, whose "the separation is wrong" charge is furniture of a specific engineering paradigm and does not travel even as a failure.

On evaluative weight it carries a real but conditional framing: "smell" and "anti-pattern" mark the anaemic case as a defect, so the label convicts — yet the entry's whole point is that the verdict is not read off the shape but off a role question, and the identical surface is a feature in a deliberate value object. So it is a charged verdict, lighter and more conditional than "theater," which keeps it off the framed pole. The remaining criteria pull firmly framed. On human_practice_bound it is bound in the decisive sense: the code exists observer-free, but "data class smell" is a judgment relative to a paradigm's expectation that a class ought to bundle data with the operations guarding its invariants — remove that OO commitment and there is no smell, only code arranged a certain way. Its institutional_origin is pronounced: the concept is Fowler's catalogued code smell, the "Anaemic Domain Model" critique, and the DDD contrast — artifacts of a software-engineering discourse and its encapsulation paradigm, not facts of nature. On vocab_travels it scores low: class, encapsulation, getters/setters, Move Method, aggregate, and record are pinned to the OO substrate. And import_vs_recognize is sharply revealing — within OO the smell is recognized as the same mechanism (beans, anaemic domain models, DDD aggregates), but beyond OO it does not transfer even as a failure: in functional programming, the relational model, and dataflow systems the same data/operation separation is the correct default, so the verdict is paradigm-local and only the parents travel.

The one portable structural skeleton is an encapsulation break with resulting coupling — state and the operations that maintain its invariants pulled apart, so consumers depend on internal structure and a layout change ripples through all of them (typically with low cohesion). That skeleton — decomposing into encapsulation, coupling, and cohesion — genuinely travels and recurs across substrates, which tempts a structural reading. But it does not lift the data-class smell off framed-leaning, because that encapsulation-and-coupling condition is exactly what the smell instantiates from those parents, not what makes "data class" itself travel: the cross-domain reach belongs to encapsulation/coupling/cohesion (and, for the organizational echoes, a separation-of-knowledge-from-action family), while the load-bearing content that makes it a smell — the paradigm-local verdict that the separation is wrong — is exactly the part that does not lift, since outside OO it is simply not a defect. Its character: a conditional, paradigm-relative defect verdict on OO code, structural only in the encapsulation-break-and-coupling condition it borrows from its parents, and framed in precisely the judgment — "this ought not be separated" — that is OO furniture and travels nowhere else.

Structural Core vs. Domain Accent

This section decides why the data class is a domain-specific abstraction and not a prime, and it carries the case for its domain-specificity — there is no separate section for that.

What is skeletal (could lift toward a cross-domain prime). Strip the object-oriented framing away and a thin relational structure survives: state and the operations that maintain its invariants are pulled apart, so the operations disperse into consumers that depend on the state's internal structure — and a change to that structure ripples through all of them. The portable pieces are abstract — a container of data with no guard over its own consistency, a set of consumers each independently re-deriving the guard, invariants centralized nowhere so the data drifts into inconsistent states, and a change cost that scales with the number of dependents reaching directly in. That encapsulation-break-with-coupling skeleton is genuinely substrate-portable: it decomposes, as the entry stresses, into more general catalog structures, and its dependency-ripple face recurs wherever consumers bind to internals. That recurrence is mechanism, which is why the entry decomposes the smell into its parents — but it is the core the data class shares, not what makes it a data class.

What is domain-bound. Almost everything that makes the concept a data class in particular is object-oriented-design furniture and none of it survives extraction intact: the accessor-only class of fields, getters, and setters (a struct wearing object syntax); the paradigmatic encapsulation commitment that a class ought to bundle data with the operations guarding it; the role question and its two verdicts (anaemic entity versus deliberate value object); the Move Method remedy and the record-formalization remedy (Java record, Kotlin data class, C# record struct, Python @dataclass); and the empirical cases — the DateRange re-validated three divergent ways, the JPA Account beside its @Service layer, Fowler's Anaemic Domain Model. These are the worked vocabulary, remedies, and cases the discipline actually studies. The decisive test — sharper here than for most entries — is that the verdict is paradigm-local: remove the OO encapsulation expectation and there is no smell, only code arranged a certain way; in functional programming, the relational model, and dataflow systems the same data/operation separation is the default and correct arrangement, so nothing migrated and nothing is wrong.

Why this does not clear the prime bar. A prime is a relational structure whose vocabulary travels and whose cross-domain transfer is recognition of the same mechanism, not analogy. The data class's transfer is bimodal, and unusually sharp about it. Within object-oriented software the mechanism travels intact — the accessor-only diagnostic, the role question, the anaemic-versus-record branch, and the Move-Method and formalization remedies all keep their meaning across OO design proper, layered enterprise architecture, Domain-Driven Design, and refactoring practice, because all share the encapsulation commitment the verdict depends on. Beyond OO the concept does not transfer even as a failure: where the encapsulation expectation is absent, separating data from operations is correct by design, so "data class" names no defect there at all — carrying it across borrows the shape while importing an expectation the other paradigm never made, which is analogy, and a misleading one. And when the bare structural lesson is needed cross-domain, it is already carried, in more general form, by the parents the data class instantiates: state and its guarding operations pulled apart is a break in encapsulation; consumers depending on internal structure is coupling; a container of data with no operations of its own is low cohesion — with a separate separation-of-knowledge-from-action family for the organizational echoes. The cross-domain reach belongs to those parents; "data class," as named, carries the paradigm-local verdict that the separation is wrong — OO furniture that does not and should not travel.

Relationships to Other Abstractions

Local relationship map for Data ClassParents 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.Data ClassDOMAINPrime abstraction: Coupling — is part ofCouplingPRIMEPrime abstraction: Information Hiding — presupposesInformationHidingPRIMEDomain-specific abstraction: Code Smell — is a kind ofCode SmellDOMAIN

Current abstraction Data Class Domain-specific

Parents (3) — more general patterns this builds on

  • Data Class is a kind of Code Smell Domain-specific

    Data Class is the code-smell species whose passive fields and accessors provide defeasible evidence that behavior and invariant enforcement migrated into consumers.

  • Data Class is part of Coupling Prime

    Cross-class coupling is a constituent of the data-class smell because consumers reach into exposed state and carry the displaced behavior.

  • Data Class presupposes Information Hiding Prime

    The data-class smell presupposes information hiding because its verdict is that an entity exposes representation and invariants that its behavioral surface was expected to conceal.

Hierarchy paths (7) — routes to 7 parentless roots

Not to Be Confused With

  • Deliberate value object / record type. A class that is intentionally pure data — fields, value-equality, immutability, no behavior — declared with Java record, Kotlin data class, C# record struct, or Python @dataclass. Its surface is byte-for-byte identical to an anaemic entity, but it is a feature, not a smell, because no invariant-owning behavior was ever supposed to live there. Tell: answer the role question — should this class enforce invariants on its own data? No → deliberate record (correct); yes → data-class smell (defect). Same shape, opposite verdict.

  • Data Transfer Object (DTO). A pattern object whose job is to carry data across a boundary (an API response, a serialization layer) with no domain behavior by design. Like the value object, its behavior-lessness is intentional and correct — it is not an entity that lost its methods. Tell: is the class a domain entity that should guard its own state (data-class smell if anaemic) or a boundary-crossing carrier never meant to hold logic (DTO, legitimate)?

  • Feature Envy. The complementary sibling smell: a method that reaches into another class's data more than its own, i.e. behavior sitting in the wrong place. Data class is the mirror image — a class stripped of the behavior that its consumers now hold. They frequently co-occur (the envious method is often the migrated behavior), but one names the misplaced method, the other the emptied class. Tell: is the defect a method using another object's internals (Feature Envy) or a class with no methods of its own while consumers re-derive its invariants (data class)?

  • Anaemic Domain Model. Not a distinct concept but the same smell named at architectural scale — persistence entities reduced to beans while a service layer holds the business rules. It is the data-class condition spread across a layered system, resolved by the same Move Method (at scale). Tell: it differs only in granularity (one class versus a whole entity layer), not in mechanism; treat it as the architecture-level manifestation of this entry, not a separate thing.

  • God class / large class. The opposite smell — a class with too much behavior and responsibility, hoarding operations that belong elsewhere. Where the data class has migrated its behavior out, the god class has absorbed behavior in. Tell: does the class do almost nothing while consumers do its work (data class), or does it do far too much, centralizing unrelated responsibilities (god class)?

  • The encapsulation / coupling / cohesion parents (umbrella). The substrate-neutral primes the smell instantiates — a break in encapsulation (state and its guarding operations pulled apart), the resulting coupling (consumers bind to internal structure), and low cohesion. Not confusable peers but the generalization: outside OO these describe the same arrangement without the "it's wrong" verdict. Tell: the parents travel to any paradigm (where the separation may be correct); "data class," treated more fully in a later section, is the OO instance carrying the paradigm-local judgment that the separation is a defect.

Neighborhood in Abstraction Space

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

Family — Unclustered & Miscellaneous (309 abstractions)

Nearest neighbors

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