Code smellDispensables
Data Class
A class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it.
- Also known as
- Anemic model
Signs
- A class has only fields, getters and setters.
- Public mutable fields or a setter for every field, including ones that should never change after creation.
- Collections are exposed directly, so callers push and splice them.
- Other classes contain methods that read three or four fields of the data class and compute something from them.
- The same calculation over the class's fields appears in several services.
Why it happens
The class started as a record for transfer or persistence, and behavior grew in the services that use it. Some architectures encourage this by putting all logic in "service" classes. ORMs and serializers that need public setters push in the same direction.
A Data Class is not always wrong. The smell is the combination: passive data here, and code elsewhere that envies it.
Example
A warehouse Shipment exposes everything, and the services do the thinking.
class Shipment {
id = "";
status: "packing" | "shipped" | "delivered" = "packing";
parcels: { weightKg: number }[] = [];
shippedAt?: Date;
}
class ShippingService {
ship(s: Shipment): void {
if (s.status !== "packing" || s.parcels.length === 0) throw new Error("cannot ship");
s.status = "shipped";
s.shippedAt = new Date();
}
totalWeight(s: Shipment): number {
return s.parcels.reduce((sum, p) => sum + p.weightKg, 0);
}
}
// elsewhere: s.parcels.push(...) and s.status = "delivered" with no checksMove the rules into the class and close the fields:
type Status = "packing" | "shipped" | "delivered";
class Shipment {
private _status: Status = "packing";
private _shippedAt?: Date;
private readonly _parcels: { weightKg: number }[] = [];
constructor(readonly id: string) {}
get status(): Status { return this._status; }
get parcels(): readonly { weightKg: number }[] { return this._parcels; }
addParcel(weightKg: number): void {
if (this._status !== "packing") throw new Error("shipment is closed");
this._parcels.push({ weightKg });
}
ship(now = new Date()): void {
if (this._status !== "packing" || this._parcels.length === 0) throw new Error("cannot ship");
this._status = "shipped";
this._shippedAt = now;
}
totalWeight(): number {
return this._parcels.reduce((sum, p) => sum + p.weightKg, 0);
}
}Treatment
- If fields are public, use Encapsulate Field.
- If a collection is exposed, use Encapsulate Collection and add
add/removemethods. - If client code computes from the class's fields, use Move Method to put that code on the class. Use Extract Method first if only part of a client method belongs there.
- If a field should not change after construction, use Remove Setting Method.
- If an accessor is no longer needed outside, use Hide Method.
Payoff
- Invariants are enforced in one place instead of trusted to every caller.
- Duplicate calculations in services collapse into one method.
- The class's API shows what you can do with it, not only what it stores.
When to ignore
- It is a data transfer object, API payload, event or database row whose job is to carry data across a boundary. Keep it plain, and put behavior in the domain type on the other side.
- It is an immutable record (
readonlyfields, TypeScript type, Java record, Python frozen dataclass) with no invariants to protect. - In functional style, data and functions are separate by design. Group the functions in one module next to the type.
Agent checklist
- You confirmed the class is a domain object, not a DTO, event or persistence row that must stay plain.
- Each moved method uses mostly the data class's fields, not the service's.
- Every caller that mutated fields directly now goes through a method, and the type checker confirms no direct writes remain.
- Serialization, ORM mapping and test fixtures still work with the new private fields.
- Tests cover the invariants the new methods enforce.
Treatments
- Encapsulate FieldMake a public field private and give clients accessor methods, so the class controls how its data is read and changed.
- Encapsulate CollectionStop a getter from handing out a mutable internal collection; return a read-only view and add methods to add and remove items.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Remove Setting MethodDelete the setter for a field that should only be set at creation, and assign it in the constructor so it cannot change later.
- Hide MethodReduce a method's visibility to private or protected when nothing outside the class needs it, shrinking the public surface.
See also
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Primitive ObsessionDomain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.