Low-level designCore6 min
Value objects, equality and immutability
Some objects are things with an identity; others are values, like a time slot or an amount, where two equal ones are interchangeable. Give values equality by their contents and make them immutable, and two classic bugs disappear: duplicates that aren't noticed, and changes that leak through a shared object.
A room booked by time slot
Meetings have time slots, and the room refuses a slot that's already booked.
1 · The situation
Booking a meeting room
A coworking space books its meeting room by time slot. A TimeSlot holds a start and an end, in minutes after midnight; a Meeting has a title and a slot; the Room keeps the slots already booked and refuses a slot that's taken. The first version makes TimeSlot an ordinary class that can be moved:
class TimeSlot {
// Minutes after midnight.
private int start;
private int end;
TimeSlot(int start, int end) {
this.start = start;
this.end = end;
}
void moveBy(int minutes) {
start += minutes;
end += minutes;
}
String text() {
return String.format("%02d:%02d-%02d:%02d", start / 60, start % 60, end / 60, end % 60);
}
}
class Room {
private final List<TimeSlot> booked = new ArrayList<>();
boolean book(TimeSlot slot) {
if (booked.contains(slot)) return false;
booked.add(slot);
return true;
}
}Drawn from the code: meetings and the room both refer to TimeSlot objects.
2 · The change
A double booking, and a meeting that moved by itself
Two people book 09:00 to 09:15. Then a pairing session is set up "at the same time as standup", by passing standup's slot, and later moved half an hour on:
09:00-09:15 booked 09:00-09:15 booked Standup at 09:30-09:45 Pairing at 09:30-09:45
The second booking of the same slot went through, and moving the pairing session moved standup too.
Two different mistakes, both about what a time slot is:
- Equality by identity. The room asks "is this slot already booked?", and the list answers by comparing objects, not times. Two separately created 09:00 to 09:15 slots are different objects, so the duplicate slips through.
- A shared, changeable object. Both meetings hold the same slot object. Moving it for one meeting moves it for both, from code that never mentioned standup.
3 · The fix
Make the slot a value
A time slot has no identity worth keeping: any 09:00 to 09:15 is as good as any other. Model it as a value object: equal when its contents are equal, and never changed after it's made.
In Java a record compares its fields in equals() and hashCode(); Python's frozen dataclass does the same, and C++ asks for a defaulted comparison. The room keeps a set of slots, so a second 09:00 to 09:15 is recognized as already there.
record TimeSlot(int start, int end) {
TimeSlot {
if (end <= start) throw new IllegalArgumentException("a slot must end after it starts");
}
TimeSlot movedBy(int minutes) {
return new TimeSlot(start + minutes, end + minutes);
}
String text() {
return String.format("%02d:%02d-%02d:%02d", start / 60, start % 60, end / 60, end % 60);
}
}
class Room {
private final Set<TimeSlot> booked = new HashSet<>();
boolean book(TimeSlot slot) {
return booked.add(slot);
}
}09:00-09:15 booked 09:00-09:15 refused, already booked Standup at 09:00-09:15 Pairing at 09:30-09:45
The duplicate is refused, and standup stays at 09:00.
4 · Values and entities
Is it a value, or a thing with an identity?
Aspect | Value object | Entity |
|---|---|---|
Two with the same contents | Are the same value: interchangeable | Are still two things |
Equality | By contents | By identity, or by an id |
Changes | Never; make a new one | Over its life, through its methods |
Examples | A time slot, an amount of money, a date, a color | A meeting, a booking, a member, a gift card |
The meeting stays an entity: two meetings at the same time with the same title are still two meetings. It holds a value, and changing its time means replacing that value. The page Classes, objects and state is about entities; this page is about the other half.
After the fix: TimeSlot is a record with no way to change it; Meeting holds one and replaces it to reschedule.
5 · Equality
Equal values must hash alike
Hash-based collections, a HashSet or a Python set, find an object by its hash first and its equality second. So a class that defines equality by contents must define its hash from the same contents: two equal slots with different hashes would land in different buckets, and the duplicate would slip through again. Java records and Python frozen dataclasses generate both together; written by hand, equals() and hashCode() must always change together.
Language | Value equality | Immutability |
|---|---|---|
Java | A | Record fields are final; validate in the compact constructor |
C++ |
| Values are copied by default; offer no setters |
Python |
|
|
JavaScript | None for objects: |
|
6 · Immutability
Why never changing is worth it
- Safe to share. An immutable slot can be passed to any number of meetings, as the fixed version does; nobody can change it under the others.
- Safe in sets and maps. A value whose contents changed after it went into a hash set would sit in the wrong bucket, invisible to its own lookups.
- Validated once. The constructor checks that a slot ends after it starts, and that stays true forever.
- Safe across threads, since there's nothing to update; the concurrency pages build on that.
The cost is a new object for every change. For small values like slots and amounts that's cheap and usually invisible. Immutability can also be shallow: a Java record holding a list still lets that list change, so a value object should hold only values, or copies of collections.
7 · In practice
Values in the garage
In the parking-lot problem, a spot size, a license plate and a fee amount are values; a ticket and a spot are entities. Make the values immutable with equality by contents, and the entities hold them.
Design the parking lot with its values and its entities apart.
Check yourself
4 questions
Takeaways
Remember this
- A value object is defined by its contents: two equal ones are interchangeable.
- Give values equality by contents, and a hash that agrees with it.
- Make values immutable: a change makes a new value, and the holder takes it.
- A shared mutable object leaks changes to every holder; an immutable one can be shared freely.
- Entities have identities and change over time; they hold values.