Serialization, Content Negotiation & Compression
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.
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.
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
- Where is the bottleneck: bandwidth (mobile, cross-region) or CPU (very high QPS)?
- How does each format handle schema evolution when producers and consumers deploy independently?
- 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
- What lets a consumer decode an event written by a producer it has never coordinated with?
- Where is the enforcement point that blocks a breaking schema change before any event is produced?
- Why does raw JSON on Kafka fail this requirement even though it is flexible?