Skip to content
aipatterns.dev

Refactoring techniqueOrganizing Data

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.

Problem

A Project knows its Tasks, 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

TypeScript
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

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

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.

Treats these smells

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.