# Data Class

> A class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it.

- kind: smell
- category: dispensables
- aliases: Anemic model
- canonical: https://aipatterns.dev/refactoring/smells/data-class
- inspired-by: https://refactoring.guru/smells/data-class

## 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.

```ts title="shipment.ts"
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 checks
```

Move the rules into the class and close the fields:

```ts title="shipment.ts"
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](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md).
- If a collection is exposed, use [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md) and add `add`/`remove` methods.
- If client code computes from the class's fields, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put that code on the class. Use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) first if only part of a client method belongs there.
- If a field should not change after construction, use [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md).
- If an accessor is no longer needed outside, use [Hide Method](https://aipatterns.dev/refactoring/techniques/hide-method.md).

## 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 (`readonly` fields, 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.
