Crux Read real .proto snippets — field-tag edits, reserved, streaming signatures — and predict the wire-level behaviour or the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
A protobuf schema is a contract you read at code-review time, before the corruption ships. Read each diff or signature, work out what it does on the wire, and pick the change a senior would actually approve.
Goal
Practise the review you run on every proto PR: spot the field-tag traps, confirm a removal is reserved correctly, and read a streaming signature for the call shape it really declares.
An old client wrote an Order with priority in field 4. A service deployed with this new schema reads that message. What does it see, and why?
Heads-up Names never travel on the wire; only numbers do. The old priority bytes are tagged 4, and field 4 is now coupon, so the value is decoded as a coupon.
Heads-up Decoding does not compare against the old type — it reads the wire bytes against the current schema. There is no type-mismatch error here, just a value steered into the wrong field.
Heads-up Reusing the same numbers for different fields is the trap, not the safeguard. Wire compatibility is per-number identity, not per-number existence.
A new field is proposed: add `string email = 2;` back, because the team wants email again. Does this compile, and is it the right move?
Heads-up reserved is enforced by protoc — declaring a reserved number or name and then using it is a compile error. That enforcement is the whole reason to reserve on removal.
Heads-up Type match is irrelevant; old peers that still send the original field 2 would write into the new email regardless. reserved exists to make this impossible.
Heads-up Editing the reservation away to reuse the number defeats the safety mechanism — that edit is the mistake reserved is meant to stop a reviewer from approving.
Map each RPC to its call shape and the position of the stream keyword.
Heads-up The stream keyword changes the shape: stream Message means a sequence of Message, not one. Only an RPC with no stream on either side is unary.
Heads-up stream on the return type (the second one) means the server streams responses — server streaming. Client streaming puts stream on the request type.
Heads-up Bidirectional streaming is one RPC with stream on both the request and the response — exactly what Live declares. It rides one multiplexed HTTP/2 stream.
A client sends UpdateProfile with bio set to the empty string and marketing_opt_in omitted. The server wants to PATCH only the fields the client actually meant to change. What does the schema let it distinguish?
Heads-up Plain proto3 scalars have no presence: an empty string is indistinguishable from an unset one. Only optional (or message-typed) fields carry a presence bit.
Heads-up proto3 re-added explicit presence via the optional keyword; marketing_opt_in here does carry presence. It is the un-optional bio that cannot be distinguished.
Heads-up An empty-string scalar serializes the same as a defaulted/unsent one — the server cannot infer intent from it. That ambiguity is exactly why PATCH endpoints use optional or a FieldMask.
Recap
Reviewing protobuf is reading numbers, not names: a renumber diff silently steers old values into the wrong field, so evolution must be append-only with reserved guarding removals (and protoc enforces the reservation). A streaming signature is read by which side carries stream — request for client, response for server, both for bidi. And proto3 presence is real but opt-in: plain scalars cannot tell unset from default, so PATCH-style APIs reach for optional or a FieldMask. Catch these at review time, before the corruption deploys.