# Change Bidirectional Association to Unidirectional

> Remove a back-reference that no code needs any more, so two classes stop depending on each other.

- kind: technique
- category: organizing-data
- canonical: https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional
- inspired-by: https://refactoring.guru/change-bidirectional-association-to-unidirectional

## Problem

Two classes point to each other, but only one direction is used in practice. The unused direction still costs sync code, creates a dependency cycle between modules, complicates serialization and keeps objects alive longer than necessary.

## Solution

**Find the code that uses the unwanted direction, give it another way to get what it needs, then delete the back-reference and its sync logic.**

## Before

```ts
class Device {
  owner: Account | null = null;
  constructor(readonly serial: string) {}

  notificationEmail(): string | null {
    return this.owner?.email ?? null;
  }
}

class Account {
  private devices = new Set<Device>();
  constructor(readonly id: string, readonly email: string) {}

  register(device: Device): void {
    this.devices.add(device);
    device.owner = this;
  }

  firmwareAlert(version: string): string[] {
    return [...this.devices].map((d) => `${d.notificationEmail()}: ${d.serial} needs ${version}`);
  }
}
```

## After

```ts
class Device {
  constructor(readonly serial: string) {}
}

class Account {
  private devices = new Set<Device>();
  constructor(readonly id: string, readonly email: string) {}

  register(device: Device): void {
    this.devices.add(device);
  }

  firmwareAlert(version: string): string[] {
    return [...this.devices].map((d) => `${this.email}: ${d.serial} needs ${version}`);
  }
}
```

## Why

- `Device` no longer imports `Account`, which breaks a module cycle.
- `register` can't leave the two sides out of sync.
- Device objects serialize cleanly and can be reused for unowned stock.

## Steps

1. Find every reader of the back-reference (`device.owner`). Include templates, serializers and tests.
2. For each reader, supply the value another way: pass it as a parameter, read it from the controlling side, or look it up through a repository. Run the tests after each.
3. When no reader remains, remove the code that assigns the back-reference. Run the tests.
4. Remove the field.

## Watch out

- Hidden readers are common: ORM mappings, GraphQL resolvers, admin panels, logging. Search for the field name as a string too.
- If removing the reference forces a slow lookup on a hot path, keep it.
- Persistence: in a relational schema, the foreign key usually lives on the "many" side. Removing the object reference does not mean dropping the column.
- The inverse is [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md).

## Related

- Fixes [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md) and removes [Dead Code](https://aipatterns.dev/refactoring/smells/dead-code.md) when the link is unused.
- Inverse: [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md).

## Agent checklist

- [ ] Every reader of the back-reference was found and given an alternative.
- [ ] No hot path now does an expensive lookup instead.
- [ ] Sync code that assigned the back-reference is deleted.
- [ ] Database columns and ORM mappings were handled deliberately.
- [ ] Tests pass.
