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.
💵 The Ten-Rupee Note in Your Pocket
Sanjana buys a samosa for ₹10 outside her college in Hyderabad. She hands the shopkeeper, Iqbal bhai, a slightly crumpled ₹10 note. In the evening she buys chai, and Iqbal bhai gives her a ₹10 note as change — a different note than the one she gave him in the morning, crisper, with someone's phone number scribbled in one corner.
Does Sanjana complain? "Bhaiya, this is not MY note! Mine had a small fold in the corner and no phone number!"
Of course not. The thought is absurd, and it is worth pausing on why it is absurd. Nobody on earth tracks which exact ₹10 note they have. Every ₹10 note is worth exactly ₹10, and any one is freely exchangeable for any other. The serial number exists — the Reserve Bank tracks notes when it must — but in daily life the serial number carries no meaning. What matters about the note is its value, not its identity. If Iqbal bhai photocopied a note (legally, say, for a poster), nothing about anyone's wealth would change; if he returned a different but equal note, nobody would ever know or care.
Now compare with Sanjana's college ID card. One day the office mixes up cards and hands her Deepika's ID — same course, same year, same surname even. Is that fine? Absolutely not. An ID card stands for one particular person. Sanjana cannot attend her exam with Deepika's card, however "equal" the two cards look. An ID card has identity. A ₹10 note does not.
This single distinction — note versus ID card — is the whole lesson of today's refactoring. Some objects in our programs are ID cards: customers, students, accounts. We must track exactly which one we hold. But many objects are ₹10 notes: money amounts, dates, phone numbers, map coordinates. For these, asking "which one?" is meaningless — only "what value?" makes sense.
Change Reference to Value is for the moment we catch ourselves treating a ₹10 note like an ID card — sharing one tracked instance, mutating it in place, comparing by "is it the same object?" — when the concept never had identity in the first place. The fix: make it a small, immutable value, equal to any other value with the same contents.
🧠 What is Change Reference to Value?
Change Reference to Value is a refactoring from Martin Fowler's Refactoring book. The situation: an object is being managed as a reference — created through a registry or factory, shared between holders, mutated in place, compared by identity — even though the concept it represents has no real identity. A currency. A date range. A money amount. There is no such thing as "THE five hundred rupees"; any ₹500 equals any other.
Treating such a concept as a reference drags in costs it never needed: a registry to look instances up, careful lifecycle management, and — worst of all — the danger of aliasing: one holder mutates the shared object, and every other holder's data silently changes underneath them.
The move has three parts:
- Make the object immutable. Remove setters. Operations that used to modify the object now return a new object instead — like exchanging a note rather than scribbling on it.
- Give it value equality. Two objects with equal contents are equal, full stop. Override
equals/==and the hash code so they agree. - Delete the sharing machinery. The registry, the factory cache, the lookup — all gone. Anyone may create the value freely, anywhere, because copies are harmless.
After the refactoring, the object is a proper value object in Martin Fowler's sense: small, immutable, compared by contents. His favourite example is Money — an amount plus a currency — and his rule of thumb from the Value Object bliki is crisp: values override the equality method; entities usually don't. Equality is literally the fingerprint that tells you which kind of object you are looking at.
Remember our one guiding question, the same one from Change Value to Reference:
"Is it THE same thing, or just AN equal thing?"
If "AN equal thing" is good enough — any equal note will do — the concept is a value, and this refactoring brings the code in line with that truth.
Fowler's practical trigger in the catalog: a reference object is "too small and infrequently changed to justify managing its life cycle." If you are maintaining a registry, identity comparisons, and mutation discipline for something like a date or an amount, you are paying an ID-card price for a ₹10-note concept. Stop paying. Make it a value.
College corner: the bug class this refactoring kills is called aliasing: two names (variables, fields) bound to one mutable object, so a write through one name is observed through the other. Aliasing is not always wrong — it is exactly what entities want — but unintended aliasing is among the hardest bugs to trace, because the corrupting write happens far away in code that "obviously" touches something else. Functional programming languages solve it wholesale by making everything immutable; value objects import that solution surgically, applying immutability only where identity is meaningless. An immutable object is also automatically thread-safe: no writes means no data races, no locks, no memory-visibility puzzles.
🔔 When do we need it?
Reach for this refactoring when you notice:
- Aliasing bugs ("spooky action at a distance"). You change the discount on one order, and a completely different order's total changes too — because both secretly shared one mutable object. This is the loudest alarm bell.
- A registry guarding something identity-free. A cache that interns
CurrencyorMoneyinstances "so that==works" is machinery propping up the wrong model. Value equality removes the need. - Identity comparisons on concepts without identity. Code asking
a === borReferenceEquals(a, b)for amounts or dates, and breaking whenever an equal-but-distinct instance appears (for example, after JSON deserialization — the same logical value comes back as a different object, and identity checks fail). - Distributed or parallel trouble. Fowler notes references are especially awkward across processes: you cannot share a pointer over the network. Values travel happily — serialize, send, recreate, still equal.
- Collections misbehaving. You want
Moneyas a map key or in a set, but without value-basedequals/hashCodethe collection treats equal amounts as different entries.
This refactoring also completes the journey that Primitive Obsession starts: first you wrap a raw number into a Money class (Replace Data Value with Object), and then this refactoring makes that class a true value — immutable, content-equal, freely copyable. Likewise, when Data Clumps get bundled into an Address or DateRange, those bundles almost always deserve value semantics, not reference semantics.
When not to use it: when the object genuinely has identity and shared changing state — a customer, an account, a live order. Making those values would mean updates stop propagating, which resurrects the stale-photocopy bugs from the inverse article. The seesaw tilts on the concept, not on fashion.
A quick triage table for the alarm bells:
| Alarm bell | What it sounds like | Diagnosis |
|---|---|---|
| Spooky action at a distance | "I changed order A and order B's total moved" | Aliased mutable value — apply this refactoring |
| Interning cache for amounts | "We cache Money so == works" | Machinery propping up wrong equality |
| Deserialization breaks checks | "After JSON load, === started failing" | Identity used where value equality belongs |
| Set holds duplicates | "Two equal DateRanges in one set" | Missing content-based equals and hash |
| Updates stop propagating | "I made it immutable and screens went stale" | Too far! That concept is an entity — inverse refactoring |
👀 Before and after at a glance
Here is the disease in TypeScript — a shared, mutable Money handed out by a cache:
// BEFORE — Money treated like an ID card
class Money {
constructor(
public amount: number,
public currency: string,
) {}
addTo(delta: number): void {
this.amount += delta; // mutates in place!
}
}
// A registry "interning" money so identity comparison works:
const cache = new Map<string, Money>();
function money(amount: number, currency: string): Money {
const key = `${amount}-${currency}`;
if (!cache.has(key)) cache.set(key, new Money(amount, currency));
return cache.get(key)!;
}
const samosaPrice = money(10, "INR");
const chaiPrice = money(10, "INR"); // SAME shared object as samosaPrice
samosaPrice.addTo(5); // samosa got costlier...
console.log(chaiPrice.amount); // 15 — chai price changed too! Spooky!
console.log(samosaPrice === chaiPrice); // true, but only by accident of the cacheIqbal bhai would be horrified: raising the samosa price somehow raised the chai price, because both prices were secretly the same note. After — Money becomes an honest ₹10 note:
// AFTER — Money as a value: immutable, content-equal, freely created
class Money {
constructor(
public readonly amount: number,
public readonly currency: string,
) {
Object.freeze(this); // belt and braces: runtime immutability
}
plus(delta: number): Money {
return new Money(this.amount + delta, this.currency); // a NEW note
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
const samosaPrice = new Money(10, "INR");
const chaiPrice = new Money(10, "INR"); // separate object, and that is FINE
const newSamosaPrice = samosaPrice.plus(5);
console.log(chaiPrice.amount); // 10 — untouched, no spooky action
console.log(samosaPrice.equals(chaiPrice)); // true — AN equal thing
console.log(samosaPrice === chaiPrice); // false — and nobody caresThe cache is deleted. The mutation is gone — "changing" money means making new money. And equality now asks the only question that ever made sense for money: same amount, same currency?
🪜 Step-by-step, the safe way
-
Confirm the concept has no identity. Ask the rupee-note question with your team: if two instances have equal fields, is there ANY situation where the program must tell them apart? If yes — stop, it is an entity; you may even need the inverse refactoring. If no — proceed.
-
Make the object immutable, one door at a time. Remove or privatize setters. For each mutating method, add a non-mutating twin that returns a new instance, migrate callers to the twin, then delete the mutator:
class Money {
// Step 2 (intermediate): both methods exist while callers migrate
addTo(delta: number): void {
this.amount += delta; // old, dying
}
plus(delta: number): Money {
return new Money(this.amount + delta, this.currency); // new, growing
}
}-
Mark fields readonly once no mutator remains. The compiler now proves immutability for you and lists any stragglers as errors.
-
Implement value equality and matching hash. Every defining field participates in both. In TypeScript that is an
equalsmethod (and atoKey()string if you need map keys, since JS maps compare objects by identity). In C#, Java, Kotlin, and Python, use the language feature —record,record class,data class,@dataclass(frozen=True)— and the compiler writes correct, always-in-sync equality and hashing. -
Dismantle the registry. Make the constructor public, delete the cache and factory lookup, and let callers write
new Money(10, "INR")freely. Watch the diff shrink — deleted machinery is the visible reward of this refactoring. -
Replace identity checks with value checks. Search for
===,ReferenceEquals,is(Python) on this type and convert each toequals/==. Then run the full test suite, paying special attention to any code that relied on shared mutation propagating — that code needs a real entity instead, or an explicit update path.
Step 4 hides the sharpest knife: equality and hash code MUST agree. If two objects are equal but hash differently, dictionaries and sets silently lose them — items vanish, lookups miss, and no exception is ever thrown. Hand-written equality drifts every time a field is added. Strongly prefer compiler-generated equality (C# records, Python frozen dataclasses, Kotlin data classes) over hand-rolled Equals/GetHashCode pairs.
College corner: the equals-hash contract has a second, subtler clause: the hash of an object used as a dictionary key must never change while it is in the dictionary. Hash containers store each entry in a bucket chosen by its hash at insertion time; mutate a field that feeds the hash, and lookups now compute a different bucket — the entry still exists but can never be found again. This is why "mutable value object" is a contradiction in terms: value equality demands field-based hashing, and field-based hashing demands the fields never change. Immutability is not a stylistic preference here; it is what makes value semantics mathematically coherent.
🏫 A bigger real-life example
A travel-booking app in Kochi models a DateRange for hotel stays. Someone made it a shared, mutable reference years ago, "to save memory," and the bugs have been biting since:
// BEFORE — a shared, mutable DateRange
class DateRange {
constructor(
public checkIn: string, // "2026-07-01"
public checkOut: string, // "2026-07-05"
) {}
extendByDays(days: number): void {
const out = new Date(this.checkOut);
out.setDate(out.getDate() + days);
this.checkOut = out.toISOString().slice(0, 10); // mutation!
}
}
class Booking {
constructor(
public readonly hotel: string,
public stay: DateRange,
) {}
}
// The bug: a "copy" that is not a copy
const stay = new DateRange("2026-07-01", "2026-07-05");
const goa = new Booking("Sea Breeze, Goa", stay);
const munnar = new Booking("Hill View, Munnar", stay); // same object reused!
goa.stay.extendByDays(2); // guest extends Goa stay...
console.log(munnar.stay.checkOut); // 2026-07-07 — Munnar booking changed too!A guest extends a beach holiday in Goa, and a hill station in Munnar quietly extends a different booking by two days. The hotel in Munnar bills for nights nobody slept. After making DateRange a value:
// AFTER — DateRange as an immutable value
class DateRange {
constructor(
public readonly checkIn: string,
public readonly checkOut: string,
) {
if (checkOut <= checkIn) {
throw new Error("Check-out must be after check-in");
}
Object.freeze(this);
}
extendByDays(days: number): DateRange {
const out = new Date(this.checkOut);
out.setDate(out.getDate() + days);
return new DateRange(this.checkIn, out.toISOString().slice(0, 10));
}
nights(): number {
const ms = +new Date(this.checkOut) - +new Date(this.checkIn);
return Math.round(ms / 86_400_000);
}
equals(other: DateRange): boolean {
return this.checkIn === other.checkIn && this.checkOut === other.checkOut;
}
overlaps(other: DateRange): boolean {
return this.checkIn < other.checkOut && other.checkIn < this.checkOut;
}
}
class Booking {
constructor(
public readonly hotel: string,
public readonly stay: DateRange,
) {}
extendStay(days: number): Booking {
return new Booking(this.hotel, this.stay.extendByDays(days));
}
}
const stay = new DateRange("2026-07-01", "2026-07-05");
const goa = new Booking("Sea Breeze, Goa", stay);
const munnar = new Booking("Hill View, Munnar", stay); // sharing is now HARMLESS
const extendedGoa = goa.extendStay(2);
console.log(extendedGoa.stay.checkOut); // 2026-07-07
console.log(munnar.stay.checkOut); // 2026-07-05 — perfectly safeNotice the chain reaction of good things. Sharing the same DateRange between two bookings is no longer a bug — immutable values can be shared by a million holders without risk, which means the original "save memory" goal is achieved better by the value design than by the mutable cache that caused the bug. Validation moved into the constructor, so a back-to-front range can never exist. And the type attracted genuinely useful behaviour (nights, overlaps) now that it is a real domain concept instead of a fragile mutable holder.
💜 The same refactoring in C#
This is where C# records shine brightest — a record is a value object kit built into the language. Immutability via init-only properties, value equality, matching hash code, ToString, and non-destructive mutation with with expressions, all compiler-generated:
public sealed record DateRange
{
public DateOnly CheckIn { get; }
public DateOnly CheckOut { get; }
public DateRange(DateOnly checkIn, DateOnly checkOut)
{
if (checkOut <= checkIn)
throw new ArgumentException("Check-out must be after check-in");
CheckIn = checkIn;
CheckOut = checkOut;
}
public int Nights => CheckOut.DayNumber - CheckIn.DayNumber;
public DateRange ExtendByDays(int days) =>
new(CheckIn, CheckOut.AddDays(days));
public bool Overlaps(DateRange other) =>
CheckIn < other.CheckOut && other.CheckIn < CheckOut;
}
var a = new DateRange(new(2026, 7, 1), new(2026, 7, 5));
var b = new DateRange(new(2026, 7, 1), new(2026, 7, 5));
Console.WriteLine(a == b); // True — value equality, FREE
Console.WriteLine(ReferenceEquals(a, b)); // False — and irrelevantAnd Money as a positional record with with-expression updates:
public readonly record struct Money(decimal Amount, string Currency)
{
public Money Plus(decimal delta) => this with { Amount = Amount + delta };
public override string ToString() => $"{Currency} {Amount:F2}";
}
var samosa = new Money(10m, "INR");
var costlier = samosa with { Amount = 15m }; // NEW value, original untouched
Console.WriteLine(samosa); // INR 10.00
Console.WriteLine(costlier); // INR 15.00
Console.WriteLine(samosa == new Money(10m, "INR")); // TrueWhy this fits the refactoring perfectly:
recordvsclassis the value/reference split in one keyword. A plainclasscompares by reference (ID card); arecordcompares by contents (₹10 note). Changing the keyword is, quite literally, Change Reference to Value.readonly record structremoves even the heap allocation — the value lives inline, ideal for hot paths where "new object per change" worried you.withexpressions are the idiomatic "exchange the note" operation: copy everything, change one field, original untouched.- Microsoft's DDD guidance for .NET recommends records for value objects, with EF Core mapping them via owned types or value converters — so the database stores plain columns while the code enjoys full value semantics.
Python gives the same kit through frozen dataclasses — immutability, content equality, and hashing in one decorator:
from dataclasses import dataclass, replace
from datetime import date
@dataclass(frozen=True)
class DateRange:
check_in: date
check_out: date
def __post_init__(self) -> None:
if self.check_out <= self.check_in:
raise ValueError("Check-out must be after check-in")
def extend_by_days(self, days: int) -> "DateRange":
from datetime import timedelta
return replace(self, check_out=self.check_out + timedelta(days=days))
@property
def nights(self) -> int:
return (self.check_out - self.check_in).days
a = DateRange(date(2026, 7, 1), date(2026, 7, 5))
b = DateRange(date(2026, 7, 1), date(2026, 7, 5))
assert a == b # content equality, generated
assert a is not b # different objects — and nobody cares
assert hash(a) == hash(b) # frozen=True makes hashing safeCollege corner: notice frozen=True is what makes the dataclass hashable by default. A mutable dataclass with eq=True gets __hash__ set to None — Python is protecting you from the broken-bucket bug described earlier, refusing to let a content-equal mutable object enter a set. The language designers encoded the equals-hash-immutability triangle directly into the decorator's defaults. When a language fights you about hashing a mutable object, it is not being pedantic; it is quoting the contract.
🛠️ IDE support
| Tool | Helpful moves |
|---|---|
| Visual Studio / Rider (C#) | Quick-action "Convert class to record" does the equality half in one click; Make readonly / Add init accessor inspections push toward immutability; Find Usages on setters locates every mutation to migrate. |
| IntelliJ IDEA (Java) | "Convert class to record" inspection (Java 16+ records); Refactor → Make Static/Immutable helpers; warnings when a record field type is mutable. |
| VS Code (TypeScript) | Mark fields readonly and let the compiler list every mutation as an error — a complete migration checklist; ESLint rules like functional/immutable-data can keep the type frozen. |
| All of them | Search for identity checks (ReferenceEquals, ===, is) on the type to find comparisons that must become value equality. |
⚖️ Benefits and risks — and the seesaw
First, the seesaw — because this refactoring and Change Value to Reference are exact inverses. One pushes a concept toward shared identity; the other pulls it back to free-floating value. The deciding question never changes: "Is it THE same thing, or just AN equal thing?"
| Question about the concept | Push to Reference (inverse article) | Pull to Value (this article) |
|---|---|---|
| Does the real world have exactly ONE of it? | Yes — one Arjun, one customer C-1 | No — any ₹10 equals any ₹10 |
| Must updates be seen by all holders? | Yes — share one instance | No — there are no updates, only new values |
| How should equality behave? | Identity: same object | Contents: same fields |
| What machinery is needed? | Registry/repository, lifecycle, locks | None — create freely, share fearlessly |
If you find yourself fighting stale copies, the seesaw must tilt toward reference. If you find yourself fighting aliasing and registries, tilt toward value. Fighting both at once usually means one concept is secretly two — an entity holding value objects inside it, like a Customer (entity) with an Address (value).
Now the ledger for this direction:
| Benefits | Risks / Costs |
|---|---|
| Aliasing bugs vanish — no holder can mutate a value out from under another. | Code that relied on shared mutation propagating silently stops "working" — find it before it finds you. |
| Equality finally means what humans expect: same contents. | Hand-written equals/hash pairs drift; prefer compiler-generated (records). |
| Immutable values are thread-safe with zero locks, and serialize/deserialize without losing meaning. | Every "change" allocates a new object — usually negligible, measurable in hot loops (use readonly record struct). |
| Registry, cache, and lifecycle machinery get deleted — less code, fewer globals. | Large objects are costly to copy and compare by contents; big aggregates may belong as references. |
| Values work correctly as dictionary keys and set members. | Choosing wrongly (an entity forced into value form) recreates the inverse article's stale-data bugs. |
🧹 Which smells does it cure?
| Smell | How this refactoring helps |
|---|---|
| Primitive Obsession | Completes the cure: the wrapped concept becomes a true value object — immutable, comparable, freely shareable. |
| Data Clumps | Bundled clumps (Address, DateRange) get the value semantics they almost always deserve. |
| Data Class | A mutable bag of getters/setters becomes an immutable type with real behaviour and honest equality. |
| Temporary Field | Mutation-in-place workflows that left half-updated state behind are replaced by atomic "build a new value" steps. |
| Shotgun Surgery | Equality, validation, and derivation logic concentrate in the value type instead of being sprinkled across holders. |
📦 Quick revision box
+--------------------------------------------------------------+
| CHANGE REFERENCE TO VALUE — REVISION |
+--------------------------------------------------------------+
| Idea : A shared, mutable, identity-tracked object that |
| has NO real identity becomes an immutable value. |
| Key Q : "Is it THE same thing, or just AN equal thing?" |
| AN equal thing -> value (this refactoring) |
| THE same thing -> reference (see the inverse) |
| Mnemonic : Rs.10 note = value; school ID card = entity. |
| Steps : 1. Confirm: no identity, equal fields = equal |
| 2. Remove mutators; add return-new twins |
| 3. Mark all fields readonly/init |
| 4. Value equality + MATCHING hash (use records!) |
| 5. Delete the registry; constructor goes public |
| 6. Replace === / ReferenceEquals with equals |
| Watch out: equal-but-different-hash kills dictionaries; |
| code that depended on shared mutation. |
| C# bonus : record / readonly record struct = the whole |
| refactoring in one keyword. |
| Inverse : Change Value to Reference (the seesaw's far end) |
+--------------------------------------------------------------+✍️ Practice exercise
A cab-fare app in Hyderabad shares one mutable Location object everywhere, and riders keep reporting that their saved "Home" location mysteriously moves:
class Location {
constructor(
public lat: number,
public lng: number,
public label: string,
) {}
moveTo(lat: number, lng: number): void {
this.lat = lat;
this.lng = lng;
}
}
const locationCache = new Map<string, Location>();
function getLocation(label: string, lat: number, lng: number): Location {
if (!locationCache.has(label)) {
locationCache.set(label, new Location(lat, lng, label));
}
return locationCache.get(label)!;
}
// The bug in the wild:
const home = getLocation("Home", 17.385, 78.4867);
const tripStart = getLocation("Home", 17.385, 78.4867); // same shared object
tripStart.moveTo(17.4474, 78.3762); // driver adjusts pickup point...
console.log(home.lat, home.lng); // Home has MOVED. Rider is not amused.Your tasks:
- Decide with the rupee-note question: does a
Location(a pair of coordinates plus a label) have identity, or is it a value? Write your one-line justification as a comment. - Make
Locationimmutable:readonlyfields, constructor validation (latitude between -90 and 90, longitude between -180 and 180), and awithCoordinates(lat, lng)method returning a newLocation. - Add
equals(other)comparing all three fields, and atoKey()string for use in maps. - Delete
locationCacheandgetLocationentirely. Fix the bug scenario and prove with a test that adjusting the pickup point leaves "Home" untouched. - Bonus in C#: write
Locationas areadonly record structwith awith-expression example, and show that two equal locations compare==true whileReferenceEquals-style identity is irrelevant. - Bonus in Python: write
Locationas a@dataclass(frozen=True)and confirm it can live safely inside aset. - Thinking question: the app ALSO has a
Driverobject. ShouldDriverget the same treatment? Answer with the seesaw — and if not, name the refactoring you would check instead.
If your fixed test shows "Home" standing still while the pickup point moves, you have understood Iqbal bhai's wisdom: exchange the note, never scribble on it.
Frequently asked questions
- How do I decide if something should be a value instead of a reference?
- Ask the rupee-note question: do you care WHICH exact one you have, or only WHAT it is worth? Nobody tracks which particular Rs.10 note is in their pocket — any equal note will do. Money, dates, coordinates, phone numbers are like that: no identity, only value. If the answer is 'any equal one will do', make it a value.
- Why must value objects be immutable?
- Because values are shared and copied freely. If a Money object could change its amount, then changing 'your' Rs.50 might silently change someone else's Rs.50 that happens to be the same object — spooky action at a distance. Immutability removes the danger completely: to 'change' a value, you create a new one, like exchanging a note.
- Does creating a new object for every change waste memory?
- Usually not enough to matter — small objects are cheap, and garbage collectors are tuned for short-lived objects. In genuinely hot paths, C# offers readonly record struct, which lives on the stack with zero heap allocation. Measure before worrying.
- What happens to equality and hashing?
- They must be based on the object's contents — every field that defines the value participates in both Equals and the hash code, and the two must agree. Get this wrong and dictionaries silently misbehave. This is why language features like C# records and Python frozen dataclasses are so valuable: the compiler generates matching equality and hashing for you.
- Is this refactoring really the inverse of Change Value to Reference?
- Yes, exactly. Change Value to Reference takes copied objects and forces them into one shared, mutable instance — for entities with identity. Change Reference to Value takes a shared instance and dissolves it into free, immutable copies — for concepts without identity. Same seesaw, opposite directions; the concept's nature decides which way to push.
Further reading
Related Lessons
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.
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.