studyDDIAChapter 5 · Encoding and Evolution

Compression Closes the Gap

On raw bytes, Avro is 2.74× smaller than JSON — the number people quote to justify switching. But JSON's bulk is repeated field names, exactly what gzip removes. Compress the batch and the advantage collapses to 1.43×: the size win was mostly compressible overhead.


Concept

Encode a batch of 10,000 records four ways and the binary formats look like a runaway win: Avro is 2.74× smaller than JSON on the raw bytes. But most of JSON's bulk is the same field names and punctuation repeated 10,000 times — which is precisely the redundancy gzip was built to crush. So gzip each blob and watch the advantage largely collapse: after compression Avro is only 1.43× smaller than JSON, because gzip already removed the very overhead that made the binary formats look good. You will predict the raw gap, then predict what survives compression — and find the size argument for binary mostly evaporates.

Provenance

Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 5 — Encoding and Evolution, §"Binary encodings" (and §"The Merits of Schemas"):

a compact encoding is not the only reason to use these formats.

The book lists schema formats' merits beyond size. This exercise pressure-tests the size merit itself: measure the raw gap, then compress and see how little of it is left.

Prerequisites

The surprise is that a "2-3× smaller" number is mostly compressible overhead. These help; each line notes where it shows up.

  1. Self-describing vs. schema-based encodings — JSON/MessagePack embed the field names in every record, so a 10,000-record batch re-sends "userName" 10,000 times; Protobuf/Avro keep the names in a schema instead. This is the redundancy that inflates the raw JSON and that gzip later removes.
  2. How gzip (DEFLATE) works — it replaces repeated byte sequences with short back-references (LZ77) plus entropy coding. Repeated field names are ideal prey, so the format with the most repetition compresses the most.
  3. Raw size vs. compressed size — "smaller on the wire" depends on whether the wire is compressed. The whole point is that these two rankings differ: the raw table and the gzipped table tell different stories.
  4. A batch, not one record — compression needs volume to find repetition, so we encode 10,000 records; on a single tiny message gzip has almost nothing to work with (and its header would cost more than it saves).
Where to learn the prerequisites The encodings (#1): DDIA §"Binary encodings", §"Protocol Buffers", §"Avro"; and the size-ladder exercise, which measures the per-record overhead directly. DEFLATE / gzip (#2): RFC 1951 (DEFLATE) and RFC 1952 (gzip); any LZ77 explainer — you only need the intuition "repeats get cheap." Why batch size matters (#3–#4): try the "Go deeper" variant that shrinks the batch to 1 record and watch the ranking flip back. If only one is new, make it #2 — that gzip preys on exactly JSON's repeated field names is the hinge of the whole result.
Environment these numbers came from Deterministic, so the byte counts reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.12.13 via uv, with msgpack, fastavro, and Protocol Buffers (grpcio-tools compiling the .proto at runtime + the protobuf runtime) — no Docker; gzip is stdlib. A batch of 10,000 user records {id, userName: "user{i}", favoriteNumber, interests[2]}, generated from a fixed formula (no randomness), gzipped at level 9.

Mental model

The same 10,000-record batch, measured twice:

We compare each format to JSON both raw and gzipped. The whole exercise is one script, code/compression_gap.py.

Setup

cd study/ddia/ch05/code
uv run --with msgpack --with fastavro --with grpcio-tools --with protobuf \
    python3 compression_gap.py

Do not read the output yet — make each prediction first.


Step 1 · predict the raw gap

Predict. Encode 10,000 records. JSON as newline-delimited objects, and Avro as concatenated schemaless values. How much smaller is the raw Avro blob than the raw JSON blob — 1.5×? 2×? 3×? Write down a number.

Step 2 · measure raw, then gzipped

Do. Run the script.

How do the four raw sizes compare — and the gzipped ones?
batch of 10,000 user records {id, userName, favoriteNumber, interests[2]} format raw vs JSON gzip -9 vs JSON -------------------------------------------------------------- JSON (ndjson) 924,160 1.00x 97,092 1.00x MessagePack 722,855 0.78x 89,076 0.92x Protocol Buffers 374,603 0.41x 75,805 0.78x Avro 337,305 0.36x 68,076 0.70x

Raw, the binary formats win big: Avro is 0.36× JSON's size — 2.74× smaller. That is the number people quote when they say "switch to Protobuf and cut your bytes in half." Look at the raw column and binary looks like an easy win.

Step 3 · predict what survives compression, then read the gzip column

Predict. Now cover the gzip columns. gzip compresses all four blobs. Does the 2.74× Avro-vs-JSON gap hold after compression, widen, or shrink?

Does the binary size advantage survive gzip?
raw: Avro is 2.74x smaller than JSON (0.36x its size). gzip: Avro is 1.43x smaller than JSON (0.70x its size) -- the gap largely closed. gzip shrinks JSON by 89% (924,160 -> 97,092 bytes): most of JSON's bulk was repeated field names + punctuation, which is exactly what gzip removes.

The gap collapses from 2.74× to 1.43×. gzip crushes the 924 KB JSON blob by 89%, down to 97 KB, because those 10,000 repeats of "userName", "favoriteNumber", "interests", and the braces and quotes are exactly the redundancy DEFLATE eats. Avro started with far less to remove (337 KB → 68 KB), so the two end up close. The raw 2.74× advantage was mostly compressible overhead — it never survived the wire once you turned compression on.


What you should see

Why

Every format stores the same values; they differ in how much self-description they add. JSON adds the most: in a 10,000-record batch the string "userName" appears 10,000 times, "favoriteNumber" 10,000 times, plus braces, quotes, colons, and commas throughout. That is why the raw JSON blob is 924 KB while Avro — which carries the field names once, in a schema the reader already has — is 337 KB. On the raw bytes, dropping repeated self-description is worth 2.74×.

But "repeated byte sequences" is the exact thing gzip's LZ77 stage is designed to eliminate: the first "favoriteNumber": costs its full length; every later copy becomes a short back-reference pointing at the first. So the format with the most repetition has the most to gain from compression. gzip takes 89% off JSON but far less off Avro, whose bytes were already close to pure payload with little for the compressor to find. The rankings that looked so different raw converge: Avro's 2.74× lead shrinks to 1.43×. Compression, in effect, gives JSON a schema of its own — a dynamically built dictionary of the repeated strings — reclaiming most of what a static schema bought Protobuf and Avro.

The boundary — binary's size win is real only where you can't compress a batch The 2.74× is an artifact of measuring uncompressed bytes; put a gzip (or brotli, or zstd) between the encoder and the wire and it falls to ~1.4×. That compression needs volume to find repetition — so binary's size edge genuinely holds for the cases gzip can't help: a single tiny message (gzip's header would cost more than it saves), a latency- or CPU-bound path where you won't pay to compress, or per-message framing on a hot queue. Outside those, choose Protobuf or Avro for their other merits — a checked schema, fast typed parsing, safe evolution, code generation — not for a size win that compression already gave you for free. That is exactly the book's point that "a compact encoding is not the only reason to use these formats."

Go deeper

Sources: DDIA 2e, Ch. 5, §"Binary encodings", §"The Merits of Schemas" · RFC 1951 (DEFLATE), RFC 1952 (gzip) · the MessagePack, Protobuf, and Avro encoding specs.