gRPC and Protocol Buffers

gRPC: RPC framework using HTTP/2 as transport, Protobuf for serialization. Each RPC = one HTTP/2 stream. Benefits: multiplexing, streaming (server/client/bidirectional), HPACK compression.

graph LR tag1["tag byte<br/>(1<<3)|0 = 0x08"] --> val1["varint: 150<br/>0x96 0x01"] tag2["tag byte<br/>(2<<3)|2 = 0x12"] --> len["length byte: 0x02"] len --> val2["raw bytes: 'A' 'l'"]

Protobuf wire format: field encoded as (field_number << 3) | wire_type varint + value. Small integers = 1 byte. Binary: 3–10× smaller than JSON, significantly faster to parse.

Backward/forward compatibility: fields identified by number, not name. Add new fields with new numbers (old code ignores them). Never reuse field numbers. Mark removed fields reserved. Rolling deploys work without coordinating client/server updates.

gRPC vs REST: gRPC for internal services (binary, streaming, generated clients, HTTP/2); REST for public APIs (JSON, browser-compatible, human-readable, broad tooling).

Worked example — wire encoding: given message User { int32 id = 1; string name = 2; } and a value id = 150, name = "Al", the field-1 tag byte is (1 << 3) | 0 = 0x08 (wire type 0 = varint), followed by 150 encoded as a two-byte varint (0x96 0x01). The field-2 tag byte is (2 << 3) | 2 = 0x12 (wire type 2 = length-delimited), followed by a length byte 0x02 and the two raw bytes A, l. Total: 8 bytes. The equivalent JSON {"id":150,"name":"Al"} is 22 bytes — roughly 3× larger, and every byte of that overhead is field names, which Protobuf never sends on the wire at all (they're only in the compiled schema).

Worked example — compatibility during a rolling deploy: a service adds a new field string email = 3 to User and deploys server-first. Old server instances and new server instances run side by side for a few minutes during the rollout. A new client sending email to an old server: the old server's generated code doesn't recognize field number 3, skips over those bytes (it knows the length from the wire-type-2 prefix), and processes the rest of the message normally — no crash, no rejected request. An old client talking to a new server simply never sets field 3, and the new server sees it as absent (default value) rather than an error. This is only safe because both sides identify fields by number, not by struct-field order or name — reusing a retired field number for a new field would silently misinterpret old data as the new type, which is why retired fields get marked reserved instead of reused.