# Change Unidirectional Association to Bidirectional

> Add a back-reference when each of two classes needs to navigate to the other, and keep both sides in sync through one owner.

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

## Problem

A `Project` knows its `Task`s, but a `Task` does not know its project. Code that holds a task and needs the project has to search all projects, or every caller has to pass the project alongside the task.

## Solution

**Add a back-reference field on the other class, and make one side responsible for updating both links.** Clients change the association only through that controlling side.

## Before

```ts
class Task {
  constructor(readonly id: string, public title: string) {}
}

class Project {
  private tasks: Task[] = [];
  constructor(readonly id: string, readonly ownerId: string) {}

  add(task: Task): void { this.tasks.push(task); }
  has(task: Task): boolean { return this.tasks.includes(task); }
}

function canEdit(task: Task, userId: string, projects: Project[]): boolean {
  const project = projects.find((p) => p.has(task));
  return project?.ownerId === userId;
}
```

## After

```ts
class Task {
  private _project: Project | null = null;
  constructor(readonly id: string, public title: string) {}

  get project(): Project | null { return this._project; }

  /** Only Project calls this. */
  _setProject(project: Project | null): void { this._project = project; }
}

class Project {
  private tasks = new Set<Task>();
  constructor(readonly id: string, readonly ownerId: string) {}

  add(task: Task): void {
    task.project?.remove(task);
    this.tasks.add(task);
    task._setProject(this);
  }

  remove(task: Task): void {
    if (this.tasks.delete(task)) task._setProject(null);
  }
}

function canEdit(task: Task, userId: string): boolean {
  return task.project?.ownerId === userId;
}
```

## Why

- Navigation from task to project is O(1) and needs no extra parameters.
- `canEdit` no longer takes the whole project list.
- One controlling side keeps the two links consistent.

## Steps

1. Add the back-reference field on the class that lacks it.
2. Choose the controlling side. In one-to-many associations it is usually the "one" side (`Project`). In many-to-many, pick either.
3. On the non-controlling side, add a narrow setter for the back-reference that only the controller calls.
4. Update the controller's modifying methods to set the back-reference. Handle moving an object from one owner to another. Run the tests.
5. Replace lookups and extra parameters in clients with the new navigation.

## Watch out

- Bidirectional links double the bookkeeping. A bug in sync leaves the graph inconsistent, which is hard to debug.
- They create reference cycles. Garbage-collected languages cope; reference-counted ones (Swift, `Rc` in Rust, C++ `shared_ptr`) need a weak side.
- Serializers that walk object graphs (`JSON.stringify`, many ORMs) loop forever or fail on cycles. Exclude the back-reference.
- Don't add one when the reverse navigation is rare. A lookup or a parameter is cheaper to maintain.
- The inverse is [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md).

## Related

- Helps with [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md) and [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md) that come from roundabout lookups.
- Inverse: [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md).
- [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md) keeps clients from bypassing the controller.

## Agent checklist

- [ ] Both directions of navigation are actually needed.
- [ ] Exactly one side controls the association; the other exposes only a narrow setter.
- [ ] Moving an item between owners updates all three objects.
- [ ] Serializers and reference-counted memory were checked for cycle problems.
- [ ] Tests cover add, remove and move.
