# Large Class

> A class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.

- kind: smell
- category: bloaters
- aliases: God Class, God Object, Blob
- canonical: https://aipatterns.dev/refactoring/smells/large-class
- inspired-by: https://refactoring.guru/smells/large-class

## Signs

- The class has dozens of fields, and groups of them share a prefix: `billingStreet`, `billingCity`, `shippingStreet`, `shippingCity`.
- Some methods use one cluster of fields and never touch the others.
- The file is the most frequently edited one in the repository, by people working on unrelated features.
- Its name is generic (`Manager`, `Service`, `Helper`, `Context`) because nothing narrower fits.
- Constructing it in a test needs a long list of collaborators, most of which the test never exercises.

## Why it happens

A class starts small with a clear job. Each new feature needs "somewhere" to live, and the class that already has the relevant data is the obvious place. Nobody creates a new class for one method, so methods accumulate. After a while the class owns several responsibilities that only coincidentally share an instance.

## Example

A user account class that also handles billing, notifications and audit logging.

```ts title="account.ts"
class Account {
  id: string;
  email: string;
  displayName: string;
  cardLast4?: string;
  cardExpiry?: string;
  stripeCustomerId?: string;
  emailOptIn = true;
  smsOptIn = false;
  phone?: string;
  auditLog: string[] = [];

  rename(name: string) { this.displayName = name; this.audit(`rename ${name}`); }
  changeEmail(email: string) { this.email = email; this.audit(`email ${email}`); }

  attachCard(token: string, last4: string, expiry: string) { /* calls payment API */ }
  chargeMonthly(amountCents: number) { /* uses stripeCustomerId */ }
  isCardExpired(today: Date): boolean { /* parses cardExpiry */ return false; }

  notify(subject: string, body: string) { /* checks emailOptIn, smsOptIn, phone */ }
  audit(entry: string) { this.auditLog.push(`${new Date().toISOString()} ${entry}`); }
}
```

The card fields and methods form one cluster, the notification preferences another. Each becomes its own class, and `Account` holds them.

```ts title="account.ts"
class PaymentProfile {
  constructor(private customerId: string, private card?: Card) {}
  attachCard(token: string, card: Card) { /* ... */ }
  charge(amountCents: number) { /* ... */ }
  isExpired(today: Date): boolean { return this.card?.expiresBefore(today) ?? true; }
}

class ContactPreferences {
  constructor(public email: string, public phone?: string, public emailOptIn = true, public smsOptIn = false) {}
  channels(): Channel[] { /* ... */ return []; }
}

class Account {
  constructor(
    readonly id: string,
    public displayName: string,
    readonly contact: ContactPreferences,
    readonly payment: PaymentProfile,
    private readonly audit: AuditTrail,
  ) {}

  rename(name: string) {
    this.displayName = name;
    this.audit.record(this.id, `rename ${name}`);
  }
}
```

## Treatment

- If a subset of fields and the methods that use them belong together, use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md).
- If some behavior applies only to certain instances (trial accounts, enterprise accounts), use [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md), or prefer composition if the variation is likely to grow.
- If clients each use a different slice of the class, use [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) so each client depends on only what it calls. This is a first step that reduces coupling before you split the implementation.
- If the large class is a UI component that also holds domain data, use [Duplicate Observed Data](https://aipatterns.dev/refactoring/techniques/duplicate-observed-data.md) to move the data into a domain object the view observes.
- Split long methods inside the class first; the clusters become easier to see.

## Payoff

- Each class can be understood, tested and changed without loading the others into your head.
- Fewer merge conflicts, because unrelated features no longer edit the same file.
- Constructors shrink and test setup becomes proportional to what is being tested.
- Duplicated logic between clusters becomes visible and removable.

## When to ignore

- A facade or composition root whose job is to wire many collaborators together. Its size reflects the number of collaborators, not tangled logic.
- Generated code (ORM models, protocol stubs) that you do not edit by hand.
- A data-heavy record that mirrors an external schema one to one. Splitting it would diverge from the schema you must match.

## Agent checklist

- [ ] You identified field clusters by checking which methods read which fields, not by guessing from names.
- [ ] Each extracted class has a name that describes one responsibility.
- [ ] Callers that reached into the moved fields now go through the new class, and no field is left duplicated in both places.
- [ ] Public API changes are either avoided (delegate from the old class) or every caller is updated in the same change.
- [ ] Existing tests pass, and the new classes have their own focused tests.
