skip to content

A content-addressed store keys artefacts by a digest of their encoded bytes, so why can two services encoding the same value produce different ids?

level: middleimportance: must knowfreq 62%

answer

  1. one value, many valid byte strings
  2. the grammar permits, it does not choose
  3. order, spacing, spelling, escapes, presence
  4. a profile turns encode into a function
  5. byte-wise member order, shortest numbers

basics

~20 s

Most encodings give the encoder freedom — member order, whitespace, number spelling, escapes, omitting defaults — so one value has many valid byte sequences. A canonical encoding fixes every one of those choices so the digest depends only on the value.

solid answer

~50 s

A published encoding usually defines a **grammar of valid byte sequences**, not a function from values to bytes. Wherever the grammar accepts more than one spelling, the writer chooses freely: the order of named members, insignificant whitespace, which of `1`, `1.0` and `1e0` it emits, whether a character is escaped, whether a field sitting at its default is written at all, and in binary forms whether a length or integer uses the shortest encoding. A digest is computed over bytes, so every one of those choices reaches the id. A *canonical* (deterministic) encoding is a profile over the encoding that decrees exactly one choice for each freedom, turning `encode` into a function; only then is `digest(encode(value))` a property of the value. It makes two encodings of one value identical — it does not make two values your domain calls equal identical.

code

json · 3 lines
json
{"amount":1,"currency":"EUR","note":"caf\u00e9"}
{"currency":"EUR","amount":1.0,"note":"café"}
{ "amount": 1e0, "currency": "EUR", "note": "café" }

go deeper

for a junior

Recall that a digest is computed over bytes, not over meaning, and that the same value can legitimately be written in more than one way. Being able to point at member order and whitespace as sources of difference is enough at this stage.

for a middle

Explain the mechanics: the encoding defines valid byte sequences and leaves the writer free wherever several are valid, and a canonical profile decrees one choice per freedom. Name at least four freedoms beyond ordering and say how each is pinned.

for a senior

Show you have debugged this. Talk about ids that were stable in one service and unstable in another, about hash-map iteration order changing between runs, and about the cross-implementation fixture corpus you would use to prove the profile actually holds.

for a principal

Own the boundary between normalising the value and canonically encoding it. Decide which equalities the domain owns, where the profile is specified and versioned, and what happens to every existing id on the day the profile changes.

## Why one value has many byte spellings An encoder turns a value into bytes; a published encoding defines which byte sequences are **valid** and what each one means. Those are not the same job. Almost every general-purpose encoding specifies a grammar and leaves the writer free wherever that grammar accepts more than one spelling of the same value. The freedoms that actually bite in production: - **Member order.** In an encoding whose objects or maps carry member names, the grammar accepts the members in any order. A writer that iterates a hash-based map can emit two different orders in two runs of the *same* build. - **Insignificant whitespace.** Indentation, a space after a separator, a trailing newline: meaning-preserving, byte-changing. - **Number spelling.** `1`, `1.0`, `1e0` and `0001` may all denote one value in a text encoding; a binary encoding commonly offers both a one-byte and a wider spelling of a small integer. Under IEEE 754, negative zero and the many bit patterns that all mean "not a number" are the same trap in binary form. - **String escapes.** A character may appear literally or as an escape; escape hex digits may be upper or lower case; only some characters *must* be escaped. - **Text normalization.** The same visible text has more than one Unicode code-point sequence (NFC against NFD), and UTF-8 turns those into different bytes. - **Presence at default.** A field omitted and the same field written carrying the value a reader would have defaulted it to usually mean the same thing to that reader, and are different bytes. - **Non-minimal framing.** Binary encodings often permit a length or tag written wider than it needs to be. ## What a canonical encoding actually is A **canonical** — or *deterministic* — encoding is not a separate format. It is a **profile over an encoding that removes each freedom by decreeing exactly one choice**, so that `encode` stops being a relation and becomes a **function** from value to bytes. Once it is a function, `digest(encode(value))` is a function of the value, which is the property a content id, a dedup key or a byte-level check actually depends on. A usable profile pins at least these: | Freedom | What the profile must decree | |---|---| | Member order | One total order — typically byte-wise over the encoded member name, never a locale-sensitive collation | | Whitespace | None at all, or one exact layout | | Integers | The shortest form that represents the value | | Floating point | One spelling per value; usually forbid the payloads that have no single spelling | | Strings | One Unicode normalization form and one escaping rule | | Presence | Either always omit a default-valued field, or always emit it | | Collections | An order for anything the domain treats as unordered | ## What it buys With the profile in force, the digest becomes a **name for the value**. That single property underwrites several everyday mechanisms at once: an artefact id in a content-addressed store, a dedup key that lets two uploads of one value share storage, a cache key that hits across machines, an idempotency key that survives a retry from a different process, and a byte-level integrity check whose expected digest can be recomputed rather than merely remembered. ## Two failure modes worth naming in the interview 1. **Encoding-equal is not domain-equal.** Canonical form makes the *representation* deterministic. It does not decide that `10.00` and `10` are the same money amount, that a list the domain treats as a set should be sorted, or that an identifier is case-insensitive. Those are decisions about the value, and they have to be made *before* encoding — usually by normalising the value itself, so that the canonical encoder is handed something already in a single form. 2. **"We sorted the keys, so we are canonical."** Ordering is the most visible freedom, not the only one. A system that sorts members but leaves number spelling, escaping, normalization and presence free still produces unstable ids; the instability just becomes rarer, which makes it harder to find. ## Where the promise comes from Encodings differ in how much of this they guarantee. Some publish a deterministic profile as part of the standard; others explicitly decline to promise that two implementations — or two versions of one implementation — emit identical bytes for the same value. Where the promise is missing, the profile becomes yours to write down, version and test, and the test that matters is cross-implementation: encode one corpus with every writer you support and compare digests, rather than asserting that a single writer agrees with itself.

  • Which member order would you specify, and why not the obvious one?
    Order by a byte-wise comparison of the encoded member names. The obvious choice — a human-friendly, locale-aware collation — is a moving target: it depends on the locale data a writer happens to carry, so two writers can disagree about the order of the same two names and produce different bytes for the same value. Byte-wise ordering over the encoded name needs no external table and is reproducible anywhere.
  • Your canonical form is agreed and stable. Which mismatches can still make two ids differ for records your business calls identical?
    Anything the profile deliberately does not touch, because it is a fact about the value rather than the encoding: a decimal carrying trailing zeros, differing letter case in an identifier your domain compares case-insensitively, a field the domain treats as a set but the model stores as an ordered list, or a timestamp recorded at a different precision. Normalise the value first, then canonically encode it.
  • How would you test that a canonical profile actually holds across the writers you support?
    With a shared corpus and a cross-implementation comparison. Keep a fixture set that exercises every pinned freedom — reordered members, alternative number spellings, escapable characters, both normalization forms, present-at-default fields — encode it with every writer, and assert that all of them produce one agreed digest per fixture. A writer asserting only that it agrees with itself proves nothing about the others.

Two clerks fill in the same form: one writes the date as 07/03, the other as 7 March. Both are correct, and the filing cabinet still ends up with two entries. A canonical encoding is the office rule that says exactly one of those spellings is the filing copy.

saying these in an interview costs you the question

  • Assumes equal values always encode to identical bytes
  • Thinks whitespace is the only source of difference
  • Says sorting the member names alone makes it canonical
  • Relies on a hash-based map's iteration order being stable
  • Believes a schema-driven encoding is deterministic by construction
  • Expects the digest to see meaning rather than bytes