The Mobile App That Could Not Update
Your gRPC backend serves an iOS app. Apple takes 2-7 days to review app updates. Meanwhile, you need to ship a backend change that adds new fields to a response and removes an old one. But 40% of your users are still on the old app version. If you break the wire format, those users see crashes. Protobuf schema evolution rules tell you exactly which changes are safe and which are not.
How Protobuf Wire Format Works
Protobuf serializes by FIELD NUMBER, not field name:
message User {
string name = 1; // field number 1
string email = 2; // field number 2
int32 age = 3; // field number 3
}
Wire format (binary):
[field_num=1][type=string][value='Alice']
[field_num=2][type=string][value='alice@x.com']
[field_num=3][type=int32][value=30]
Field NAMES are only for human readability.
The wire only sees NUMBERS and TYPES.
This is why renaming a field is safe but
changing its number is catastrophic.Safe Changes (Backwards Compatible)
SAFE - Adding a new field:
// v1 (old client knows this)
message User {
string name = 1;
string email = 2;
}
// v2 (new server adds field 3)
message User {
string name = 1;
string email = 2;
string avatar_url = 3; // NEW - old clients ignore it
}
// Old client sees unknown field 3 -> skips it. No crash.
// New client sees field 3 -> uses it. Everyone happy.
SAFE - Renaming a field:
// Wire format uses numbers, not names
string user_name = 1; -> string full_name = 1; // SAFE
SAFE - Adding a new RPC method:
// Old clients never call it, so they are unaffected
SAFE - Adding a new enum value:
// Old clients treat unknown enum values as default (0)Breaking Changes (Never Do These)
BREAKING - Changing a field number:
string name = 1; -> string name = 5; // DISASTER
// Old clients send field 1, new server expects field 5
// Data silently disappears or gets misinterpreted
BREAKING - Changing a field type:
int32 age = 3; -> string age = 3; // DISASTER
// Binary encoding for int32 and string are incompatible
// Deserialization fails or produces garbage
BREAKING - Removing a field without reserving:
// v1: string email = 2;
// v2: (removed email)
// v3: int32 login_count = 2; // REUSED field 2!
// Old clients sending email string into int32 field = crash
CORRECT way to remove:
message User {
string name = 1;
reserved 2; // lock field number forever
reserved 'email'; // lock field name forever
string avatar_url = 3;
}This is the same underlying problem you run into with any versioned contract, whether it's a REST API, a gRPC service, or an event schema flowing through Kafka. If you're weighing where these decisions belong architecturally, the piece on API Gateway, BFF, tRPC, and the Multi-Protocol Architecture covers how versioning strategies play out across different protocol layers. And if your protobuf messages are also the payloads flowing through a message broker, the compatibility rules described in AMQP, Kafka, and Event Sourcing: The Async API Story apply in a very similar way.
Schema Evolution Cheat Sheet
Change Safe? Why
---------------------- ------- ---------------------------
Add new field YES Old clients skip unknown
Rename field YES Wire uses numbers, not names
Add new RPC YES Old clients do not call it
Add enum value YES Unknown -> default (0)
Change field number NO Breaks all existing clients
Change field type NO Wire encoding incompatible
Remove field (no rsrv) NO Number might be reused
Remove field (reserved) YES Number locked, safe
Change optional->req NO Deserialization failuresInterview Tip
When discussing gRPC API evolution, say: 'Protobuf uses field numbers for wire serialization, so renaming fields is always safe. Adding new fields is the primary evolution mechanism, old clients simply ignore unknown fields. I would never reuse a deleted field number, instead I mark it reserved forever. For major breaking changes, I would create a new service version (v2.UserService) rather than modifying the existing contract. This lets me roll out changes gradually while old clients continue working.'
Architecture overview
Key Takeaway
Protobuf uses field numbers (not names) for serialization. Adding fields is safe. Changing types or numbers is breaking. Never reuse deleted field numbers, reserve them instead. These rules let you evolve gRPC services without coordinated client-server deployments.
If you want to see how this kind of contract discipline fits into the bigger picture of designing backend systems, the fundamentals covered in LLD: Master the Building Blocks of Low-Level Design are worth a look too.
