title: Lyft — Protocol Buffer Design: Principles and Practices for Collaborative Development type: source created: 2026-04-22 updated: 2026-04-22 tier: 2 company: lyft published: 2024-09-16 url: https://eng.lyft.com/protocol-buffer-design-principles-and-practices-for-collaborative-development-8f5aa7e6ed85 raw: raw/lyft/2024-09-16-protocol-buffer-design-principles-and-practices-for-collabor-1e54ad9c.md tags: [protobuf, protocol-buffers, schema-design, api-design, proto3, validation, oneof, enum, backward-compatibility, lyft, lyft-media] systems: [protobuf, protoc-gen-validate, grpc] concepts: [unknown-zero-enum-value, unit-suffix-field-naming, proto3-explicit-optional, clarity-over-efficiency-in-protocol-design, extensibility-protocol-design, backward-compatibility, schema-evolution] patterns: [oneof-over-enum-plus-field, protobuf-validation-rules, protobuf-cross-entity-constants] related: [systems/protobuf, systems/protoc-gen-validate, systems/grpc, concepts/unknown-zero-enum-value, concepts/unit-suffix-field-naming, concepts/proto3-explicit-optional, concepts/clarity-over-efficiency-in-protocol-design, concepts/extensibility-protocol-design, concepts/backward-compatibility, concepts/schema-evolution, patterns/oneof-over-enum-plus-field, patterns/protobuf-validation-rules, patterns/protobuf-cross-entity-constants]
Lyft — Protocol Buffer Design: Principles and Practices for Collaborative Development¶
Summary¶
Roman Kotenko (Lyft Media) distils Lyft Media's operational experience
designing shared proto3 protobuf schemas across mobile (iOS / Android)
and backend (Python) teams into a reusable protocol-design playbook. Two
named principles — clarity and extensibility — frame five
concrete practices: reserve 0 for an UNKNOWN enum value; prefer
oneof over "discriminator enum plus type-specific optional field";
name fields with their unit (payload_size_bytes, timestamp_ms_utc)
and use well-known types (google.protobuf.Timestamp, Duration,
StringValue) rather than raw primitives; use the optional label
(proto3 ≥ 3.15, 2021) or google.protobuf.*Value wrappers so
HasField() distinguishes absent from default; declare validation
constraints inline in the .proto with
protoc-gen-validate
(or its stable successor
protovalidate) — noting
that generated validators must be invoked explicitly, they do not
run automatically on parse. Closing pattern: use custom EnumValueOptions
extensions to attach cross-entity constants to enum values (mobile
client and server stay aligned on #tag1 / #tag2 literals through the
schema rather than by parallel code).
Key takeaways¶
-
Two load-bearing principles for protocol design: clarity + extensibility. "A well-designed protocol should define its messages in a way where it's not only explicit about which fields must be set. This prevents missetting any of the messages during implementation. In other words, good protocols leave no ambiguity for its implementers." Extensibility: "protocol structure is built with future vision and potential roadmap in mind … some foreseeable additions and breaking changes can be accounted for in advance." Protocol design puts greater emphasis on these than ordinary code because the cost of a breaking change is paid across every compiled client. (Source: sources/2024-09-16-lyft-protocol-buffer-design-principles-and-practices) Canonical concepts/clarity-over-efficiency-in-protocol-design + concepts/extensibility-protocol-design.
-
Prefer
oneofover "discriminator enum + type-specific optional field". The naive shape —enum Kind { A, B, C }plus per-kind optional fields — is implicit about which combinations are valid. Ifkind == C, mustpayload_sizebe set? What ifkind == A? Every new event-type added compounds the convoluted validation logic callers must maintain.oneofmakes the discriminator and the payload the same field; exactly one of the branches can be set; unset branches don't serialise (wire-size win). Canonical patterns/oneof-over-enum-plus-field. -
Reserve
0asUNKNOWNfor every enum. Old implementations that pre-date a new enum value receive new values as0; if0is a legitimate value, the pre-existing and new consumers silently disagree. MakingENUM_UNKNOWN = 0the first entry means the "before-this-addition" behaviour is explicit and greppable. Canonical concepts/unknown-zero-enum-value. -
Name fields with their unit / semantics, not the raw primitive type.
payload_size_bytes, notpayload_size.timestamp_ms_utc, nottimestamp. Even better: usegoogle.protobuf.TimestampandDurationwell-known types so the semantics are enforced by the type system instead of naming convention. For team-specific recurring shapes (e.g. lat/lng), promote to a shared internal well-known type. Canonical concepts/unit-suffix-field-naming. -
Proto3 deprecated
requiredfor backward-compat reasons. In proto2, a required field in a message couldn't safely be relaxed to optional without breaking every old producer — therequiredlabel was "enforced strictly by the compiler which proved hugely problematic in the long run." Proto3 droppedrequiredentirely; every field is effectively optional and scalars initialise to their type's default value. Canonical concepts/proto3-explicit-optional. -
Use the
optionallabel (proto3 ≥ 3.15, 2021) or wrapper types to distinguish absent from default. Before theoptionallabel was re-introduced in proto3,uint32 payload_size_bytescouldn't tell "0 bytes" from "unset" — both serialise as absent. Withoptional uint32 payload_size_bytes, generated code gainsHasField('payload_size_bytes'). Lyft adopted protobufs before 3.15 so their legacy convention is to usegoogle.protobuf.UInt32Value/StringValuewrappers instead; new work can use theoptionallabel. Canonical concepts/proto3-explicit-optional. -
Declare validation constraints inline in the
.protowith protoc-gen-validate (PGV) or its stable successor protovalidate. Named rule families:(validate.required) = trueon aoneofforces one of the branches to be set (default behaviour would accept none);(validate.rules).enum = { defined_only: true, not_in: [0] }closes the open-enum behaviour proto3 introduced and excludesUNKNOWN;(validate.rules).string = { min_len: 1, uuid: true, pattern: "..." }for format checks;(validate.rules).repeated = { min_len: 1, unique: true, items: {...} }for collection constraints. Critical caveat flagged verbatim — "if a message is formed in violation of the stated rules, nothing will fail until its validator is invoked" — validation is not automatic on parse; generatedValidate()/protoc_gen_validate.validator.validate(msg)has to be called at every trust boundary. Canonical patterns/protobuf-validation-rules on systems/protoc-gen-validate. -
Cross-entity constants via custom
EnumValueOptionsextensions. To align a mobile client and a server service on literal string values ("#tag1","#tag2"etc.) without defining them twice in two codebases, declare a custom extension ongoogle.protobuf.EnumValueOptions(the post picks a large-ish field number,11117, "to avoid accidental collisions") and attach[(const_value) = "#tag1"]to each enum entry. Generated descriptor APIs let any language stack read the string back. Recommended only when the constants are stable and you control both deployment ends — otherwise ordinary enum dispatch is safer. Canonical patterns/protobuf-cross-entity-constants. -
Language-dependent behaviours matter. Handling of absent vs default values, map entries with absent-value fields, namespace layout, and generated-accessor patterns differ across Python / Swift / Kotlin / Go / Java — the article names serialisation of default-valued map entries as a specific "some languages write them, some omit them" divergence. Teams shipping multi-language stacks must test cross-language round-trips rather than assume parity.
Key numbers¶
- No production metrics disclosed — this is a design-practices post, not a rollout retrospective.
- Named version: protobuf 3.15 (2021) as the release that
reintroduced the
optionalfield label on singular scalars in proto3. - Custom-option field-number guidance: "For a small project, picking
an arbitrary large prime number should be safe enough." (Lyft's
example uses
11117.)
Architecture notes¶
The article's final consolidated example:
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "google/protobuf/timestamp.proto";
import "validate/validate.proto";
extend google.protobuf.EnumValueOptions {
string const_value = 11117;
}
enum EventTag {
EVENT_TAG_UNKNOWN = 0 [(const_value) = ""];
EVENT_TAG_1 = 1 [(const_value) = "#tag1"];
EVENT_TAG_2 = 2 [(const_value) = "#tag2"];
}
message Event {
string id = 1 [(validate.rules).string = {min_len: 1}];
google.protobuf.Timestamp timestamp_utc = 2
[(validate.rules).timestamp = {required: true}];
oneof data_kind {
option (validate.required) = true;
EventDataA data_a = 3;
EventDataB data_b = 4;
EventDataC data_c = 5;
}
}
message EventDataA {}
message EventDataB {}
message EventDataC {
optional uint32 payload_size_bytes = 1;
}
Five design decisions are visible simultaneously: (1) 0 = UNKNOWN
for EventTag; (2) cross-entity constants attached via
[(const_value) = ...]; (3) unit-suffixed field names (timestamp_utc,
payload_size_bytes); (4) oneof data_kind with
option (validate.required) = true so exactly one branch must be set;
(5) optional uint32 payload_size_bytes for presence semantics on a
primitive. Well-known google.protobuf.Timestamp replaces a raw
uint64. Validation rules are inline on each field and at the oneof.
Caveats¶
- Design-practices essay, not a retrospective. No production scale, QPS, latency, or incident count. The post is a distillation of Lyft Media team conventions framed as general principles — the numbers side of this page is intentionally light.
- Lyft Media context, not Lyft-wide. The practices describe Kotenko's team specifically; the 2020 cross-reference to Michael Rebello's "Lyft's Journey Through Mobile Networking" post confirms Lyft-wide adoption of protobufs for mobile-server traffic, but this post's conventions may not be universal across all Lyft orgs.
- proto3-only. Proto2 content appears only as historical framing
for why
requiredwas dropped. - PGV → protovalidate transition is flagged but not covered in depth. PGV is cited as stable, the migration cue points at protovalidate as the modern replacement, but validation examples throughout the post use PGV syntax; teams starting fresh should evaluate protovalidate directly.
- Cross-entity constants via custom options is explicitly flagged by the author as "recommended to exercise caution … most suitable for cases where the constant values are never expected to change, or where you have complete control over deployment of entities that will be consuming the protocol" — a power-tool with enough rope to hang a team running heterogeneous clients.
- No mobile-codebase specifics. The post mentions Python, Swift, Kotlin support abstractly but all code examples are Python; iOS and Android integration details are future-work.
- Author's Medium-subscription CTA inserted mid-article — not architectural content, filtered out of the distillation.
Source¶
- Original: https://eng.lyft.com/protocol-buffer-design-principles-and-practices-for-collaborative-development-8f5aa7e6ed85
- Raw markdown:
raw/lyft/2024-09-16-protocol-buffer-design-principles-and-practices-for-collabor-1e54ad9c.md
Related¶
- systems/protobuf — Google's Protocol Buffers schema + wire format, the subject of the entire post
- systems/protoc-gen-validate — the validation plugin (PGV / protovalidate) canonicalised here
- concepts/clarity-over-efficiency-in-protocol-design — first of the two named principles
- concepts/extensibility-protocol-design — second of the two principles
- concepts/unknown-zero-enum-value — reserve 0 for UNKNOWN
- concepts/unit-suffix-field-naming —
payload_size_bytes,timestamp_ms_utc - concepts/proto3-explicit-optional —
optionallabel + wrapper types for presence semantics - patterns/oneof-over-enum-plus-field — use
oneofto make discriminator + payload the same field - patterns/protobuf-validation-rules — declarative validation
in the
.proto - patterns/protobuf-cross-entity-constants — custom
EnumValueOptionsto share literal constants - concepts/backward-compatibility / concepts/schema-evolution — the overarching axes these practices defend against
- concepts/schema-registry / concepts/contract-first-design — adjacent design disciplines
- systems/grpc — the canonical consumer of
.protofiles for RPC