Skip to content
aipatterns.dev

Refactoring techniqueOrganizing Data

Change Reference to Value

Make a small shared object immutable and compare it by content, so it can be copied freely instead of tracked by identity.

Problem

A small object, such as a currency, a date range or a geo point, is shared by reference and mutated in place. Mutating it through one holder silently changes it for every other holder, and code has to defensively copy it or look it up in a registry.

Solution

Make the object immutable, give it value equality, and replace in-place mutation with methods that return a new instance.

Before

TypeScript
class DateRange {
  constructor(public start: Date, public end: Date) {}

  extendBy(days: number): void {
    this.end = new Date(this.end.getTime() + days * 86_400_000);
  }
}

const promoWindow = new DateRange(new Date("2026-11-01"), new Date("2026-11-30"));
const blackFriday = { name: "BF", window: promoWindow };
const cyberMonday = { name: "CM", window: promoWindow };

cyberMonday.window.extendBy(2); // blackFriday's window also moved

After

TypeScript
class DateRange {
  constructor(readonly start: Date, readonly end: Date) {
    Object.freeze(this);
  }

  extendBy(days: number): DateRange {
    return new DateRange(this.start, new Date(this.end.getTime() + days * 86_400_000));
  }

  equals(other: DateRange): boolean {
    return (
      this.start.getTime() === other.start.getTime() &&
      this.end.getTime() === other.end.getTime()
    );
  }
}

const promoWindow = new DateRange(new Date("2026-11-01"), new Date("2026-11-30"));
const blackFriday = { name: "BF", window: promoWindow };
const cyberMonday = { name: "CM", window: promoWindow.extendBy(2) };

Why

  • Sharing is safe. Nothing can change the range under another holder.
  • No registry or defensive copies needed; equality is by content.
  • Values are easier to reason about in concurrent code and to cache.

Steps

  1. Check the object has no identity that matters: two instances with the same data should be interchangeable.
  2. Remove setters one at a time. Replace each mutation with construction of a new instance, and reassign at the caller. Run the tests after each.
  3. Mark fields readonly and freeze the instance if your language supports it.
  4. Add equals (and hashCode in Java-like languages). Replace identity comparisons with it.
  5. Remove any registry or lookup used to share instances.

Watch out

  • Fields holding mutable objects defeat immutability. Date is mutable in JavaScript; readonly stops reassignment, not range.start.setDate(...). Copy on the way in or use an immutable date type.
  • Don't convert real entities (a customer, an account) whose changes must be visible everywhere. Those need identity.
  • Frequent changes to a large value object allocate many copies. Usually negligible; measure if it sits in a hot loop.
  • The inverse is Change Value to Reference.

Agent checklist

  • The object has no meaningful identity.
  • All setters and mutating methods were replaced with methods returning new instances.
  • Nested mutable fields are copied or replaced with immutable types.
  • Equality is by value and identity comparisons were updated.
  • Tests pass.

Treats these smells

See also

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