Proto Evolution and Backward Compatibility
Evolve service contracts safely using field numbering rules and reserved fields.
Why Proto Evolution Matters
In a gRPC microservice world, your .proto files are a contract shared between independently deployed services. A producer service and its many consumers rarely upgrade at the exact same moment.
During a rolling deploy you will have old clients talking to new servers and new clients talking to old servers simultaneously. Evolution rules exist so neither side crashes or silently corrupts data.
- Backward compatible: new code can read data written by old code.
- Forward compatible: old code can read data written by new code (Protobuf gives this for free if you follow the rules).
Break the rules and you get mismatched fields, lost data, or deserialization that maps bytes to the wrong field.
Field Numbers Are the Real Identity
The single most important idea: on the wire, Protobuf does not send field names. It sends field numbers. The name email is just a label for humans and generated code.
Each encoded field is a tag built from (field_number << 3) | wire_type followed by its value. That means:
- Renaming a field is harmless on the wire (number unchanged).
- Changing a field's number is a breaking change — old data for number 2 will be read as a different field.
// Decode a Protobuf tag byte to see how (number, wireType) are packed.
function decodeTag(tagByte) {
const fieldNumber = tagByte >> 3;
const wireType = tagByte & 0x07;
return { fieldNumber, wireType };
}
// 0x12 = 18 = (2 << 3) | 2 => field 2, length-delimited (string/bytes)
console.log(decodeTag(0x12)); // { fieldNumber: 2, wireType: 2 }
// 0x08 = 8 = (1 << 3) | 0 => field 1, varint (int/bool/enum)
console.log(decodeTag(0x08)); // { fieldNumber: 1, wireType: 0 }All lessons in this course
- Defining Services and Messages with Protobuf IDL
- Unary, Server, Client, and Bidirectional Streaming RPCs
- Interceptors, Deadlines, and Metadata
- Proto Evolution and Backward Compatibility