# Parallel Inheritance Hierarchies

> Every new subclass in one hierarchy forces a matching subclass in another, so two trees must always grow in lockstep.

- kind: smell
- category: change-preventers
- canonical: https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies
- inspired-by: https://refactoring.guru/smells/parallel-inheritance-hierarchies

## Signs

- Two class trees share name prefixes: `EmailNotification` / `EmailNotificationRenderer`, `SmsNotification` / `SmsNotificationRenderer`.
- Adding a subclass to one tree fails at runtime or compile time until you add its twin to the other.
- A factory or map pairs each class in tree A with exactly one class in tree B.
- Code downcasts or checks `instanceof` to find the matching partner.

## Why it happens

It starts as sensible separation: data in one class, the code that acts on it in another. With two or three variants it looks clean. Each new variant then adds a pair, and the pairing rule lives only in developers' heads or in a registry. It is a special case of [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md), where the scattered edit is a whole new class.

## Example

A notification system has one tree for messages and another for senders.

```ts title="notifications.ts"
abstract class Notification {
  constructor(readonly userId: string, readonly text: string) {}
}
class EmailNotification extends Notification {
  constructor(userId: string, text: string, readonly subject: string) {
    super(userId, text);
  }
}
class SmsNotification extends Notification {}

abstract class Sender<N extends Notification> {
  abstract send(n: N): Promise<void>;
}
class EmailSender extends Sender<EmailNotification> {
  async send(n: EmailNotification) { /* SMTP with n.subject */ }
}
class SmsSender extends Sender<SmsNotification> {
  async send(n: SmsNotification) { /* SMS gateway, 160 chars */ }
}

function senderFor(n: Notification): Sender<any> {
  if (n instanceof EmailNotification) return new EmailSender();
  if (n instanceof SmsNotification) return new SmsSender();
  throw new Error("no sender"); // adding PushNotification breaks here
}
```

Collapse the pairing by moving the sending behavior onto the notification and passing the transport in:

```ts title="notifications.ts"
interface Transports {
  smtp(to: string, subject: string, body: string): Promise<void>;
  sms(to: string, body: string): Promise<void>;
}

abstract class Notification {
  constructor(readonly userId: string, readonly text: string) {}
  abstract send(t: Transports): Promise<void>;
}

class EmailNotification extends Notification {
  constructor(userId: string, text: string, readonly subject: string) {
    super(userId, text);
  }
  send(t: Transports) { return t.smtp(this.userId, this.subject, this.text); }
}

class SmsNotification extends Notification {
  send(t: Transports) { return t.sms(this.userId, this.text.slice(0, 160)); }
}
```

A push notification is now one class, not two plus a registry edit.

## Treatment

- If the second hierarchy's classes hold behavior for exactly one class in the first, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put that behavior on its partner, then delete the second tree.
- If the partner classes also carry data, use [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) before moving methods.
- If the two trees must stay separate (for example, domain objects that must not depend on infrastructure), keep one hierarchy and make the other a set of instances or functions keyed by type, so a new variant needs one registration, not a new subclass.
- If both trees vary independently (N shapes times M renderers), the pairing is real; use [Bridge](https://aipatterns.dev/design-patterns/bridge.md) so each tree grows on its own.
- If you need many operations over a stable set of types, [Visitor](https://aipatterns.dev/design-patterns/visitor.md) makes the pairing explicit and type-checked.

## Payoff

- A new variant is one class in one place.
- No registry, no `instanceof` dispatch and no runtime "missing partner" errors.
- The pairing rule is enforced by the type system rather than by memory.

## When to ignore

- The separation is a deliberate layering boundary, such as domain types that must not import email or SMS libraries. Accept the pairs, and add a test that fails when a partner is missing.
- The trees vary independently and the cross product is real. That is a Bridge, not a smell.
- There are two or three stable variants and no new ones in sight.

## Agent checklist

- [ ] You confirmed the trees are paired one-to-one, not independently varying.
- [ ] Moving behavior across does not make a domain class depend on infrastructure it should not know.
- [ ] Every `instanceof` dispatch or partner registry tied to the pairing was removed or reduced.
- [ ] A test covers each variant's behavior after the merge.
- [ ] If you kept both trees on purpose, a test fails when a new variant lacks its partner.
