Skip to main content

Serialization, Content Negotiation & Compression

Level 1: Level 1: Foundations & Mental Modelsmedium30 minserializationapi-designperformanceschema-evolution

Choose format and codec by bottleneck (JSON+Brotli public, Protobuf+zstd internal), skip compressing tiny payloads, and keep schemas evolvable with field-tag discipline.

A CPU-versus-bytes trade, decided by your bottleneck

Serialization and compression are a CPU-versus-bytes trade, and the right answer depends entirely on where your bottleneck is: bandwidth (mobile, cross-region, metered) or CPU (very high QPS internal traffic). You also have to keep schemas evolvable when producers and consumers deploy independently.

Serialization formats

  • JSON: human-readable, self-describing, universal, debuggable in a browser. But verbose (field names repeat on every object) and comparatively slow to parse. It is the correct default for public APIs where developer ergonomics and debuggability beat raw efficiency.
  • Protobuf: compact binary driven by an IDL, fast to encode/decode, with generated types. Fields are tagged by number, not name, so payloads are small. Ideal for internal high-QPS RPC (it pairs with gRPC).
  • Avro: the schema is registered centrally or travels with the data (in the file header), which makes it strong for data pipelines and Kafka, where a schema registry lets producers and consumers evolve independently.
  • Thrift: RPC plus serialization from one IDL, similar niche to Protobuf, common in older Facebook-lineage stacks.

Compression, negotiated via Accept-Encoding

  • gzip: universal and cheap, the safe default.
  • Brotli: better ratio than gzip, especially on text over HTTPS to browsers.
  • zstd: excellent ratio and speed with tunable levels, great for internal transfer where you control both ends.
Check yourself
Your API serves two payloads: a 200-byte JSON health status and a 4 MB JPEG. A teammate proposes turning on gzip for every response. What is the actual effect?

The tradeoff to state explicitly: compression and binary encoding cut bytes but add CPU, and aggressive compression can add tail latency on large responses (the compressor has to run before the first byte goes out). So set a payload-size threshold below which you do not compress (compressing a 200-byte response is a net loss), and do not double-compress already-compressed data (images, video).

Schema evolution: the part people forget

Protobuf, Avro, and Thrift all support forward and backward compatibility if you follow the rules: add only optional/new fields, and never reuse or renumber a field tag (mark removed tags reserved). That is what lets a new producer and an old consumer coexist during a rolling deploy. JSON has no built-in schema, so it relies on the tolerant-reader discipline: consumers ignore unknown fields and tolerate missing optional ones.

Putting it together with content negotiation: a public API defaults to JSON, honors Accept-Encoding to pick Brotli for browsers, and sets Vary: Accept-Encoding so a shared cache does not hand a Brotli body to a client that only speaks gzip. An internal mesh uses Protobuf with zstd because both ends are controlled and CPU/bytes dominate.

Interview nuance: the trap is "Protobuf everywhere because it is faster." On a public browser API the network savings are usually tiny relative to the developer and debugging cost, and you lose curl-ability. The senior move is to locate the bottleneck first (bandwidth vs CPU) and choose per surface.

Recap: choose format and codec by bottleneck (JSON+Brotli for public/bandwidth, Protobuf+zstd for internal/CPU), never compress tiny or already-compressed payloads, and keep schemas evolvable by adding optional fields and never reusing field tags.

Check yourself
Mid rolling deploy, half your fleet runs new code and half runs old. A teammate deleted a Protobuf field last month and today reuses its tag number, 7, for a new field with a different type and meaning. What do the old consumers do with new messages?

Apply

Your turn

The task this lesson builds to.

Choose a serialization format and compression scheme for a high-fan-out internal API and for a public mobile API, and justify each against JSON, Protobuf, Avro, Thrift, and gzip, Brotli, zstd on the size, CPU, and schema-evolution axes.

Think about

  1. Where is the bottleneck: bandwidth (mobile, cross-region) or CPU (very high QPS)?
  2. How does each format handle schema evolution when producers and consumers deploy independently?
  3. How do you pick a compression codec via Accept-Encoding without paying tail latency on large payloads?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the serialization and schema-evolution strategy for a Kafka-based event platform at LinkedIn scale, where thousands of producers emit events consumed by hundreds of independently-deployed consumers, producers and consumers upgrade on their own schedules, and a bad schema change must never break downstream consumers. Choose the format and the governance.

Think about

  1. What lets a consumer decode an event written by a producer it has never coordinated with?
  2. Where is the enforcement point that blocks a breaking schema change before any event is produced?
  3. Why does raw JSON on Kafka fail this requirement even though it is flexible?