A value type in your system is used as a key in a hashed cache, stored in a sorted collection, deduplicated after JSON serialization, and compared by client code written in three different languages. Which equivalence relations are actually in play, and how do you keep them from disagreeing?
answer
- Every 'same?' mechanism is its own partition
- Ordering equivalence can be coarser than equality
- JSON and protobuf bytes are not canonical
- Go field-wise structs, JS reference, Python identity
- One canonical relation, derive the rest, ship a conformance table
basics
~20 sAt least four: hash equality, the ordering's own equivalence, serialized-byte equality, and each client language's default structural comparison. Choose one canonical relation, derive the others from it, specify it outside any one language, and document every place a coarser relation is intended.
solid answer
~60 sEvery mechanism that answers "same or not" imposes its own partition on the value space, and they are not automatically the same partition. - **Hash equality** — the declared relation. The hash must be a coarsening of it. - **The ordering's equivalence** — "neither less nor greater" is itself an equivalence, and it can be strictly coarser: under a case-insensitive comparator a sorted set keeps one entry where a hashed set keeps two. C++20 names the choice explicitly with its strong, weak and partial ordering categories; Rust splits Eq from Ord for the same reason. - **Serialized-byte equality** — JSON has no canonical form (member order, number formatting, Unicode normalization) and protocol buffers do not promise deterministic bytes, so digest-based deduplication splits classes that value equality merges. - **Client defaults** — Go compares structs field-wise with no hook, so adding an unexported cache field silently refines the relation; JavaScript and Python compare objects by identity unless told otherwise. The deliverable is a written relation, plus a canonical byte form, not a method on one class.
go deeper
Know that a hashed set and a sorted set can disagree about whether two values are the same, and that serialized bytes are not a reliable identity.
Name the distinct relations in play and explain why the hash must be a coarsening of the declared equality.
Drive the alignment: one canonical field list, ordering consistency stated explicitly, a canonical encoding before any digest, and a shared conformance table across client languages.
Frame the whole thing as partition governance across a polyglot boundary — the relation is a published specification with tests, and type systems that separate equality from ordering categories are prior art you can point to when arguing for it.
## Equality is a partition, and you own more than one An equivalence relation cuts a value space into disjoint classes; "equal" means "same class". Once you accept that framing, the design question stops being "is my equality method right?" and becomes "how many partitions does my system contain, and are they the same one?" In a service of any size the answer is at least four, and they are produced by different teams, different libraries and different languages. ## The relations, one at a time **Hash equality.** The declared relation, plus a hash that must be a *coarsening* of it: every class maps to one bucket, though a bucket may hold several classes. This is the only relation most people write down, and it is the one everything else should be derived from. **The ordering's equivalence.** An ordering induces its own relation: two values are order-equivalent when neither precedes the other. Nothing forces that relation to coincide with equality, and useful comparators deliberately make it coarser — case-insensitive, accent-insensitive, or comparing on a subset of fields. The visible consequence is that a sorted set and a hashed set built from the same elements have different sizes, and a binary search finds a member that an equality test rejects. C++20 makes the distinction a first-class part of the type system: a three-way comparison yields *strong ordering* (equivalent implies interchangeable), *weak ordering* (equivalent but distinguishable by other means), or *partial ordering* (some pairs incomparable). Rust encodes the same idea by separating Eq from Ord and PartialEq from PartialOrd. Haskell leaves the laws to convention but names the two classes separately. A language without these categories does not remove the distinction; it just stops asking you which one you meant. **Serialized-form equality.** Systems deduplicate on bytes or on a digest of bytes because it is cheap and language-independent. It is also a *different* relation. JSON specifies no canonical form: member order is free, numbers have many spellings, strings may or may not be Unicode-normalized, and whitespace is unconstrained. Protocol buffers explicitly decline to guarantee deterministic serialization — map field order and unknown-field retention can vary between library versions and even between runs. So byte equality is strictly finer than value equality: it splits classes your code considers single. Anything that keys on a digest — content-addressed storage, cache keys, idempotency keys — inherits that split, and a retry that re-serializes the same value can land in a different class. That is the single systems consequence worth stating; the rest of the analysis is about types, not infrastructure. **Client-language defaults.** Each consuming language brings a default relation for a type nobody customised. Go compares structs field-wise with no user hook, so the relation is exactly the field list: adding an unexported cached field silently refines the partition, and adding a slice, map or function field makes the type non-comparable and breaks compilation instead. JavaScript compares objects by reference, so two decoded copies of the same payload are never equal without a hand-written comparer. Python compares by identity unless the type defines otherwise. Three clients, three partitions, none of them the one you wrote. ## Keeping them aligned The design move is to name one relation canonical and derive everything else from it, in this order. 1. **Define the canonical relation as business identity**, in prose, over a named field list, independent of any implementation language. It is the specification; the code in each language is a rendering of it. 2. **Derive the hash from exactly that field list**, so the coarsening relationship holds by construction. 3. **Decide, explicitly, whether the ordering's equivalence is meant to coincide with equality.** If it is, say so and test it. If it is deliberately coarser, say that too and name the container semantics it changes — otherwise the divergence will be discovered as a missing row. 4. **Publish a canonical byte form** if anything deduplicates or digests: sorted members, a fixed number format, a stated Unicode normalization. Digest identity is then a rendering of the canonical relation rather than a fourth independent one. 5. **Exclude derived and mutable state.** Anything computed, cached or timestamped is not identity, and including it makes the partition change under the container's feet. 6. **Give clients a conformance test, not prose alone** — a table of value pairs with expected verdicts that each language's implementation must reproduce. This is what turns "we all use the same equality" from an assumption into a checked property. ## The failure signature When these relations drift, the symptom is never an exception. It is a set difference between two services that reports phantom additions and deletions, a cache whose hit rate is inexplicably low, or a deduplication step whose output is larger than its input. Each of those is a partition mismatch, and each is diagnosed by asking which relation each side actually implemented — not by reading one equality method more carefully.
- A comparator deliberately ignores case, so the sorted set is smaller than the hashed set built from the same data. Is that a bug?Only if it is undocumented. A coarser order-equivalence is a legitimate design choice — it is exactly what a weak ordering means — but it must be stated, because it changes container semantics: the sorted set deduplicates values the hashed set keeps, and a search can succeed where an equality test fails. The defect is the silence, not the coarseness.
- Why is deduplicating on a digest of serialized bytes risky even when every producer runs the same code?Because the serializers do not promise a canonical byte form. Member order, number spelling, Unicode normalization and unknown-field handling can vary across library versions and, for map fields, between runs of the same binary. Byte identity is therefore finer than value identity, so equal values can land in different classes and a retry can create a duplicate. The fix is a specified canonical encoding, applied before the digest.
saying these in an interview costs you the question
- Assuming an ordering's equivalence must match equality
- Treating serialized bytes as a canonical representation of a value
- Writing the canonical relation as a method in one language and expecting clients to match
- Including derived, cached or timestamp fields in identity
- Expecting a partition mismatch to surface as an exception rather than as wrong data