Change Value to Reference: One Office File, Not Twenty Photocopies
Change Value to Reference explained simply — why duplicate copies of the same entity go stale, and how a shared single instance via a registry or repository keeps data consistent.
📄 Twenty Photocopies of Arjun
Arjun studies in class 7B at a school in Lucknow. His record — address, blood group, parent's phone number — sits in a file in the school office, maintained by Mr. Tripathi, the office clerk who has run that office for twenty-two years. Now imagine the school does something strange: every classroom, the sports room, the library, and the canteen each keep their own photocopy of Arjun's record. Twenty photocopies, all crisp, all identical, all correct — on the day they were made.
For a while, nothing goes wrong. Then Arjun's family shifts house, from Aliganj to Gomti Nagar, right across the city. His father comes to the office and updates the address. Mr. Tripathi carefully corrects the office file. The office file is now correct... but the library's photocopy still shows Aliganj. So does the sports room's. So do seventeen others, sitting quietly in drawers, each one now confidently wrong.
When the library posts a reminder about an overdue book, it goes to the old house in Aliganj, where a puzzled new tenant reads about a book he never borrowed. Worse: one afternoon Arjun twists his ankle at football practice, and the school nurse needs his parent's phone number right now. She has three photocopies in front of her — from the sports room, the medical room, and her own drawer — and they show two different numbers. Which one does she trust, with a crying child on the bench?
The photocopies have gone stale. And notice the deep reason why: there is only one real Arjun. Twenty pieces of paper were pretending to be him, but the moment his details changed, the pretence collapsed. Copies of a changing thing always drift apart. It is not bad luck; it is mathematics — every update must now be delivered to twenty places, and the first missed delivery creates two versions of the truth.
The fix every school actually uses: one file in the office, and everyone refers to it. The library does not store Arjun's address; it stores "Arjun, admission number 1024 — see office file." When the address changes, Mr. Tripathi changes it in one place, and every department instantly sees the new truth, because they all look at the same file.
In code, this is Change Value to Reference: stop making fresh copies of an entity that has one real identity, and make everyone share a single instance.
🧠 What is Change Value to Reference?
Change Value to Reference is a refactoring from Martin Fowler's Refactoring book. The situation: your program creates many separate objects that all represent the same real-world thing. Ten orders for customer C-1 each construct their own Customer object. Each copy is correct at birth and wrong soon after — because the real customer's data changes, and copies do not hear about it.
The move: route all creation through a single access point — a factory, registry, or repository — that returns the same object every time it is asked for the same identity. After the refactoring, all ten orders point at one Customer instance. Update her credit limit through any order, and every other order sees it immediately, because they are all looking at the same office file.
This refactoring is really a lesson about a deep question:
"Is it THE same thing, or just AN equal thing?"
- A value is "an equal thing". Any ₹10 amount equals any other; copies are harmless. Dates, money, phone numbers, coordinates — these have value equality.
- A reference (an entity, in domain-driven design language) is "THE same thing". There is exactly one customer C-1, one student with admission number 1024. These have identity. Two objects claiming to be C-1 is a lie waiting to become a bug.
Refactoring Guru puts it neatly: a reference is when one real-world object corresponds to one object in the program (users, orders, products); a value is when one real-world object corresponds to many little program objects (dates, amounts, addresses). Eric Evans' DDD book draws the same line between entities and value objects.
The test is mutability plus identity. Ask two questions: (1) Does this thing have ONE true identity in the real world? (2) Can its data change, and must everyone see the change? Two yeses = make it a reference, shared through one access point. Two noes = keep it a value and read the twin article, Change Reference to Value.
College corner: in formal terms, the photocopy problem is a consistency problem under replication. Distributed-systems courses teach that the moment you replicate mutable state, you must choose an update-propagation strategy, and every strategy has costs — that is the heart of the CAP theorem. Inside one process, Change Value to Reference is the cheapest possible strategy: do not replicate at all. One canonical instance means consistency is free, because there is nothing to synchronize. The pattern enforcing this is the Identity Map (catalogued in Fowler's Patterns of Enterprise Application Architecture): a table from identity key to instance, consulted before any construction.
🔔 When do we need it?
Watch for these situations:
- Stale-copy bugs. Support updates a customer's email in one screen; another screen still shows (and uses!) the old one, because each screen built its own object. The classic photocopy symptom — the nurse with two phone numbers.
- Mutation that must propagate. A loyalty tier upgrade, a changed credit limit, a corrected address — when one part of the program writes and other parts must read the fresh value, copies are structurally wrong.
- A value object that grew up. Often this follows Replace Data Value with Object: you wrapped a customer name into a
Customerclass (good!), but thenCustomergained mutable state like purchase history. A value that accumulates changing, shared data is asking to become a reference. - Memory waste from heavy duplicates. Ten thousand orders each holding their own copy of the same 200-field
Customeris pure waste; one shared instance plus ten thousand pointers is cheap. - Identity confusion in collections. Code tries to count "how many distinct customers" and gets the wrong answer, because the same person appears as five different objects.
This refactoring also indirectly serves the smells: Primitive Obsession often hides identity in loose strings (customerId passed around raw, with entities rebuilt from it everywhere), and Data Clumps of customer fields copied into every order are exactly the photocopies this refactoring removes.
When not to use it: if the object is small, immutable, and identity-free (money, a date), sharing buys nothing and costs machinery. That is the seesaw with the inverse refactoring — we will weigh it properly in the Benefits and risks section.
A symptom-to-diagnosis table for quick triage:
| Symptom you see | Likely cause | This refactoring helps? |
|---|---|---|
| Two screens disagree about one customer | Each screen built its own copy | ✅ Yes — share one instance |
| Distinct-customer count is too high | Same person as five objects | ✅ Yes — one object per identity |
| A money amount changed "by itself" | Shared mutable value | ❌ No — you need the inverse refactoring |
| Update in one batch invisible in another | Copies per batch | ✅ Yes — registry per identity |
| Tests pass alone, fail together | Static registry leaking between tests | ⚠️ Refactor the registry to an injected repository |
👀 Before and after at a glance
In TypeScript. Before — every order manufactures a brand-new Customer:
// BEFORE — every classroom photocopies the record
class Customer {
constructor(
public readonly id: string,
public name: string,
public loyaltyPoints: number = 0,
) {}
addPoints(points: number): void {
this.loyaltyPoints += points;
}
}
class Order {
public customer: Customer;
constructor(customerId: string, customerName: string) {
// A fresh Customer per order — even for the same person!
this.customer = new Customer(customerId, customerName);
}
}
const a = new Order("C-1", "Dana");
const b = new Order("C-1", "Dana");
a.customer.addPoints(100);
console.log(b.customer.loyaltyPoints); // 0 — b's photocopy never heard the news
console.log(a.customer === b.customer); // false — two objects pretending to be one personAfter — creation funnels through a repository that hands out one instance per identity:
// AFTER — one office file, everyone refers to it
class Customer {
constructor(
public readonly id: string,
public name: string,
public loyaltyPoints: number = 0,
) {}
addPoints(points: number): void {
this.loyaltyPoints += points;
}
}
class CustomerRepository {
private readonly byId = new Map<string, Customer>();
getOrCreate(id: string, name: string): Customer {
let customer = this.byId.get(id);
if (!customer) {
customer = new Customer(id, name);
this.byId.set(id, customer);
}
return customer;
}
}
class Order {
public readonly customer: Customer;
constructor(repo: CustomerRepository, customerId: string, customerName: string) {
this.customer = repo.getOrCreate(customerId, customerName);
}
}
const repo = new CustomerRepository();
const a = new Order(repo, "C-1", "Dana");
const b = new Order(repo, "C-1", "Dana");
a.customer.addPoints(100);
console.log(b.customer.loyaltyPoints); // 100 — everyone sees the office file
console.log(a.customer === b.customer); // true — THE same thingThe line a.customer === b.customer is the heart of it. Before: false — two photocopies. After: true — one shared file. Identity in the real world is now mirrored by identity in memory.
College corner: that === check is reference equality — "are these the same heap address?" For entities, reference equality is exactly the equality you want within one process, because it mirrors real-world identity. Across process boundaries (a JSON API, a message queue), pointers cannot travel, so identity must be carried by the key — C-1 — and re-anchored to a local instance through the repository on arrival. This is why entities always carry an explicit ID field while value objects usually do not: the ID is identity made serializable.
🪜 Step-by-step, the safe way
-
Pick the identity key. What makes two of these "THE same thing"? Usually an ID — admission number, customer code, GST number. If there is no natural key, mint one. Without a key there is no way to know which requests should share.
-
Choose the owner of the lookup. Three common homes: a static factory method on the class (quick, but global), a registry object, or — best for real applications — a repository created by your application and passed in (dependency injection). Prefer the injected repository; static registries make tests fight each other.
-
Introduce the access method beside the constructor. Do not remove the constructor yet. Add
getOrCreatethat checks the map and creates only if absent:
class CustomerRepository {
private readonly byId = new Map<string, Customer>();
getOrCreate(id: string, name: string): Customer {
const existing = this.byId.get(id);
if (existing) return existing;
const fresh = new Customer(id, name);
this.byId.set(id, fresh);
return fresh;
}
}-
Replace
new Customer(...)call sites one by one withrepo.getOrCreate(...). Compile and test after each replacement. For code that never mutates customers, behaviour is identical so far. -
Close the side door. Once no caller uses
newdirectly, make the constructor private (easy in C#; in TypeScript, export a creation interface rather than the class, or mark the constructorprivateand expose a staticcreatethat only the repository module uses). Now the registry is the only way in, and the one-instance-per-identity rule cannot be bypassed. -
Audit the mutation paths. This is the step people skip. Some old code may have relied on having an independent copy — for example, a "what-if" calculation that temporarily tweaked a customer's discount. With sharing, that tweak now leaks to everyone. Find every write to the shared object and ask: should the whole program see this? If not, give that code an explicit copy.
The behaviour change hides in step 6. Before the refactoring, mutating "your" customer affected only you. After it, mutating the customer affects EVERYONE holding it — which is exactly what you wanted for real updates, and exactly what you did not want for temporary local fiddling. Also: once instances are shared, two threads can now race on the same object. If your code is multi-threaded, add synchronization or confine mutations to one place.
🏫 A bigger real-life example
A coaching-class management app in Kota tracks batches and students. Each Batch was building its own Student objects from enrolment rows, so the same student attending Physics and Maths existed twice — and fee payments recorded in one batch never showed in the other. The accountant spent every Saturday reconciling numbers that should never have disagreed:
// BEFORE — each batch photocopies its students
class Student {
constructor(
public readonly admissionNo: string,
public name: string,
public feesPaid: number = 0,
) {}
payFees(amount: number): void {
this.feesPaid += amount;
}
}
class Batch {
public students: Student[] = [];
enroll(admissionNo: string, name: string): Student {
const s = new Student(admissionNo, name); // fresh copy every time!
this.students.push(s);
return s;
}
}
const physics = new Batch();
const maths = new Batch();
const arjunInPhysics = physics.enroll("A-1024", "Arjun");
const arjunInMaths = maths.enroll("A-1024", "Arjun");
arjunInPhysics.payFees(5000);
console.log(arjunInMaths.feesPaid); // 0 — the accountant is now confusedAfter introducing a StudentDirectory (our office file room, with Mr. Tripathi behind the counter):
// AFTER — one directory, every batch refers to it
class StudentDirectory {
private readonly byAdmissionNo = new Map<string, Student>();
getOrRegister(admissionNo: string, name: string): Student {
let student = this.byAdmissionNo.get(admissionNo);
if (!student) {
student = new Student(admissionNo, name);
this.byAdmissionNo.set(admissionNo, student);
}
return student;
}
count(): number {
return this.byAdmissionNo.size; // distinct students — finally a true answer
}
}
class Batch {
public students: Student[] = [];
constructor(private readonly directory: StudentDirectory) {}
enroll(admissionNo: string, name: string): Student {
const student = this.directory.getOrRegister(admissionNo, name);
this.students.push(student); // a pointer to the one file, not a photocopy
return student;
}
}
const directory = new StudentDirectory();
const physics = new Batch(directory);
const maths = new Batch(directory);
physics.enroll("A-1024", "Arjun");
maths.enroll("A-1024", "Arjun");
physics.students[0].payFees(5000);
console.log(maths.students[0].feesPaid); // 5000 — both batches see the truth
console.log(directory.count()); // 1 — one Arjun, as in real lifeTwo bugs died in one refactoring: fee payments are now visible everywhere, and "how many students do we have?" finally gives the real number. Notice also that the directory is injected into each Batch — not a static global — so a test can build its own fresh directory and never collide with another test.
💜 The same refactoring in C#
C# lets us enforce the funnel with a private constructor — nobody can sneak past the repository:
public class Student
{
public string AdmissionNo { get; }
public string Name { get; private set; }
public decimal FeesPaid { get; private set; }
// Private: the directory is the ONLY gate.
private Student(string admissionNo, string name)
{
AdmissionNo = admissionNo;
Name = name;
}
public void PayFees(decimal amount) => FeesPaid += amount;
public class Directory
{
private readonly Dictionary<string, Student> _byAdmissionNo = new();
public Student GetOrRegister(string admissionNo, string name)
{
if (!_byAdmissionNo.TryGetValue(admissionNo, out var student))
{
student = new Student(admissionNo, name); // allowed: nested class
_byAdmissionNo[admissionNo] = student;
}
return student;
}
public int Count => _byAdmissionNo.Count;
}
}
var directory = new Student.Directory();
var a = directory.GetOrRegister("A-1024", "Arjun");
var b = directory.GetOrRegister("A-1024", "Arjun");
a.PayFees(5000m);
Console.WriteLine(b.FeesPaid); // 5000 — same office file
Console.WriteLine(ReferenceEquals(a, b)); // True — THE same thingThree C# notes:
ReferenceEquals(a, b)is the C# way to ask "THE same object?" — it is the exact opposite of what arecordgives you. Records compare by contents (value semantics); entities likeStudentdeliberately keep the default reference equality. Choosingclasswith a private constructor here, andrecordforMoneythere, is the entity/value split written in language keywords.- The nested
Directoryclass is a tidy trick: a nested class may call the private constructor, so the funnel is compiler-enforced without exposing creation to anyone else. - ORMs already do this. Entity Framework Core's change tracker is an identity map: within one
DbContext, querying student A-1024 twice returns the same tracked instance. When you refactor toward repositories, you are aligning your in-memory model with what your ORM does at the database boundary.
If instances are shared across threads, wrap the dictionary access in a lock or use ConcurrentDictionary.GetOrAdd — shared mutable state is the price of sharing, and we pay it with synchronization.
The same funnel in Python, where the directory doubles as the only sanctioned creator:
class Student:
def __init__(self, admission_no: str, name: str) -> None:
self.admission_no = admission_no
self.name = name
self.fees_paid = 0
def pay_fees(self, amount: int) -> None:
self.fees_paid += amount
class StudentDirectory:
def __init__(self) -> None:
self._by_admission_no: dict[str, Student] = {}
def get_or_register(self, admission_no: str, name: str) -> Student:
if admission_no not in self._by_admission_no:
self._by_admission_no[admission_no] = Student(admission_no, name)
return self._by_admission_no[admission_no]
directory = StudentDirectory()
a = directory.get_or_register("A-1024", "Arjun")
b = directory.get_or_register("A-1024", "Arjun")
a.pay_fees(5000)
assert b.fees_paid == 5000 # one shared file
assert a is b # Python's identity check — THE same thingCollege corner: Python's is operator is pure reference equality (same object id), while == calls __eq__ and defaults to identity unless overridden. The entity/value split therefore maps to: entities rely on default __eq__ (identity) and are not given a custom __hash__ based on fields; value objects override __eq__ by contents and must keep __hash__ consistent with it. Mixing these up — say, hashing a mutable entity by its mutable fields — corrupts every set and dict that holds it the moment a field changes, because the object now lives in the wrong hash bucket.
🛠️ IDE support
There is no one-click "Change Value to Reference" command, but IDEs automate the key sub-steps:
| Tool | Helpful moves |
|---|---|
| Visual Studio / Rider (C#) | Find Usages on the constructor lists every new Customer(...); Change Signature helps thread the repository parameter through; Rider's Replace constructor with factory method refactoring creates the funnel for you. |
| IntelliJ IDEA (Java) | Refactor → Replace Constructor with Factory Method is built in — exactly step 3 of our recipe — then Find Usages to migrate call sites. |
| VS Code (TypeScript) | Mark the constructor private; the compiler then lists every new call site as an error — a free, complete to-do list for step 4. |
| All of them | After migration, search for new Customer / new Student in the whole solution to confirm the side door is closed. |
⚖️ Benefits and risks — and the seesaw
First, the seesaw. Change Value to Reference and Change Reference to Value are perfect inverses — one refactoring undoes the other. They sit on two ends of one seesaw, and the question that tilts it is always the same: "Is it THE same thing, or just AN equal thing?"
| Value (copies) | Reference (shared) | |
|---|---|---|
| Real-world meaning | "An equal thing" — any ₹10 equals any ₹10 | "THE same thing" — there is one Arjun |
| Equality | By contents | By identity (same object) |
| Mutability | Immutable — replace, never modify | Mutable — update in place, all see it |
| Best for | Money, dates, phone numbers, addresses | Customers, students, accounts, orders |
| Machinery | None — create freely | Registry/repository + lifecycle care |
This article's refactoring pushes things to the right column. The inverse article pushes them back left. Neither side is "better" — entities belong right, values belong left, and bugs live wherever a concept sits on the wrong side.
Now the honest ledger for this direction:
| Benefits | Risks / Costs |
|---|---|
| Updates propagate automatically — stale-copy bugs become impossible. | Shared mutable state: an accidental write is now visible everywhere. |
| The model tells the truth: one real entity = one object. | A static registry acts like a global — prefer injected repositories. |
| Memory savings when many holders share one heavy entity. | Lifecycle questions appear: when is an instance evicted? Memory can grow forever. |
| Creation, lookup, and lifetime are centralized in one place. | Threads racing on one shared object need synchronization. |
| Counting and grouping by entity finally give correct answers. | Serialization breaks naive identity — across processes, the ID (not the pointer) carries identity. |
🧹 Which smells does it cure?
| Smell | How this refactoring helps |
|---|---|
| Primitive Obsession | Loose customerId strings, with entity data rebuilt around them everywhere, become one real shared entity behind a repository. |
| Data Clumps | Customer fields copy-pasted into every order collapse into a single reference to one object. |
| Duplicate Code | "Build a customer from these fields" logic, repeated at every creation site, moves into one factory/repository. |
| Data Class | The entity gains a guarded lifecycle and real behaviour instead of being raw copyable data. |
| Shotgun Surgery | Changing how an entity is created or cached becomes one edit in the repository. |
📦 Quick revision box
+--------------------------------------------------------------+
| CHANGE VALUE TO REFERENCE — REVISION |
+--------------------------------------------------------------+
| Idea : Many copies of one real entity -> ONE shared |
| instance (office file, not photocopies). |
| Key Q : "Is it THE same thing, or just AN equal thing?" |
| THE same thing -> reference (this refactoring) |
| AN equal thing -> value (see the inverse) |
| Trigger : mutable data + identity + stale-copy bugs |
| Steps : 1. Pick the identity key (ID) |
| 2. Choose lookup owner (prefer injected repo) |
| 3. Add getOrCreate beside the constructor |
| 4. Migrate new X(...) call sites one by one |
| 5. Make the constructor private |
| 6. Audit mutations + add thread safety |
| Watch out: hidden global registries, eviction/lifetime, |
| code that secretly relied on private copies. |
| Inverse : Change Reference to Value (the seesaw's far end) |
+--------------------------------------------------------------+✍️ Practice exercise
A food-delivery app in Indore models restaurants like this:
class Restaurant {
constructor(
public readonly fssaiLicense: string, // unique license number
public name: string,
public isOpen: boolean = true,
public rating: number = 0,
) {}
}
class DeliveryOrder {
public restaurant: Restaurant;
constructor(license: string, name: string) {
this.restaurant = new Restaurant(license, name); // photocopy!
}
}The bug report: a restaurant closes for the night, the app marks isOpen = false — but only on ONE order's copy. Other screens still show it open, and new orders keep arriving at a closed kitchen. The owner is standing in a dark kitchen watching order notifications ping.
Your tasks:
- Create a
RestaurantRegistrywithgetOrRegister(license, name)that returns one instance per FSSAI license. Make it an injected dependency, not a static. - Migrate
DeliveryOrderto use the registry, then makeRestaurant's constructor inaccessible from outside. - Write a test: create two orders for the same license, set
isOpen = falsethrough one, and assert the other sees it (and that both hold===the same object). - Audit question: the app has a "simulate surge pricing" feature that temporarily bumps a restaurant's rating in a what-if calculation. What goes wrong after your refactoring, and how do you fix that one spot?
- Bonus in C#: enforce the funnel with a private constructor and a nested
Registryclass, and makeGetOrRegisterthread-safe withConcurrentDictionary.GetOrAdd. - Seesaw question: the same app has a
DeliveryFeeamount attached to each order. Should it also go behind a registry? Answer with the key question — and if your answer is "it is AN equal thing", you already know which article to read next.
If your test in step 3 prints true for the === check, the photocopies are gone: there is one office file now, and Mr. Tripathi keeps it perfectly.
Frequently asked questions
- What is the difference between a value and a reference object?
- Ask: 'Is it THE same thing, or just AN equal thing?' A reference object has identity — there is exactly ONE real customer C-1, so the program should hold one object for her, and everyone points at it. A value object has no identity — any Rs.10 amount equals any other, so copies are fine. References share; values copy.
- When do copies actually become a bug?
- The moment the data can CHANGE. Ten read-only copies of a customer's name are just wasteful. But if the customer's credit limit can change, ten copies means nine of them go stale after every update. Mutable shared state is the trigger — that is when you need one shared instance.
- Is the registry just a global variable in disguise?
- A static registry can behave like one, yes — that is the main risk. Tests interfere with each other, and instances live forever. The cleaner version is a repository or identity map owned by your application (and injected as a dependency), not a static dictionary. Same idea, controlled lifetime.
- How does this relate to databases and ORMs?
- Very directly. ORMs like Entity Framework implement an 'identity map' — within one DbContext, loading customer C-1 twice returns the SAME tracked object. The ORM is doing Change Value to Reference for you. Problems appear when you bypass it and construct duplicate entities by hand.
- What if I need this across multiple servers?
- In-memory sharing only works inside one process. Across servers, 'one shared instance' becomes 'one row in the database' or 'one record in a cache' — identity is preserved by the ID, not by the object pointer. Each server still benefits from an identity map locally, but the database is the true single file.
Further reading
Related Lessons
Change Reference to Value: Any ₹10 Note Is As Good As Another
Change Reference to Value explained simply — how to turn a shared, mutable reference object into a small immutable value object with content-based equality, with TypeScript and C# record examples.
Replace Data Value with Object: Give Your Data a Proper Home
Replace Data Value with Object explained simply — how to grow a plain string or number into a small class with validation and behaviour, with TypeScript and C# record examples.
Self Encapsulate Field: Let One Gatekeeper Guard Your Data
Self Encapsulate Field explained simply — why a class should read and write its own fields through getters and setters, with safe steps, TypeScript and C# examples.
Primitive Obsession: When Everything Is Just a String or a Number
Primitive Obsession explained simply — why plain strings and numbers hide bugs, and how value objects like Money and Address make code safe and clear.