Defining a .proto File
A .proto file begins with a syntax declaration, almost always syntax = "proto3"; today, followed by an optional package declaration that namespaces your messages to avoid collisions with other .proto files in a large codebase, and language-specific options like go_package or java_package that control where generated code lands. After that setup, the bulk of the file is made of message definitions describing data shapes and service definitions describing the callable RPC methods, and by convention each field within a message gets both a descriptive name and a unique positive integer tag starting from 1.
Cricket analogy: The package declaration in a .proto file is like a team naming itself 'Mumbai Indians' in the IPL so its players and stats never get confused with a similarly named club from a different league, keeping every reference unambiguous.
Field Rules and Naming Conventions
In proto3, every field is singular by default unless marked repeated for a list, and fields 1 through 15 should be reserved for your most frequently set fields since they encode using only a single byte of tag overhead, while field numbers 16 and above cost two bytes, making the assignment order a small but real performance decision in high-volume messages. Style convention favors snake_case for field and message field names in the .proto source (like user_id, not userId), because protoc's code generators automatically convert those names into each target language's idiomatic casing, such as userId in Java/JS or UserId in Go.
Cricket analogy: Giving your top-order batsmen the lowest, most efficient batting slots (1 to 3) so they get the most overs is like assigning your most frequently used fields the low tag numbers 1-15, which cost only one byte to encode.
Defining Services
A service block groups related RPC methods under one logical API, and each rpc line names the method plus its request and response message types, optionally prefixed with the stream keyword on either side to declare server, client, or bidirectional streaming. Good .proto design mirrors good API design: keep services focused on one bounded context (an OrderService shouldn't also handle user authentication), use a top-level request/response wrapper message for every method even when it only has one field today, since that gives you room to add parameters later without changing the method's signature.
Cricket analogy: Grouping RPC methods under one OrderService is like a franchise having a dedicated batting coach who only handles batting drills, not also fielding and bowling — a focused role rather than one person doing everything.
syntax = "proto3";
package orders.v1;
option go_package = "github.com/example/orders/v1;ordersv1";
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc ListOrders (ListOrdersRequest) returns (stream Order);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderLine lines = 2;
}
message OrderLine {
string sku = 1;
int32 quantity = 2;
}
message CreateOrderResponse {
Order order = 1;
}
message ListOrdersRequest {
string customer_id = 1;
}
message Order {
string order_id = 1;
string customer_id = 2;
repeated OrderLine lines = 3;
string status = 4;
}Wrapping every RPC method's input and output in dedicated request/response messages (CreateOrderRequest, not a bare string) is a widely recommended convention, since it lets you add new fields to a method later without breaking its signature.
Field numbers 19000 to 19999 are reserved by the Protocol Buffers implementation itself and cannot be used in your own message definitions; the compiler will reject them.
- A .proto file starts with a syntax declaration, an optional package namespace, and language-specific code generation options.
- Messages describe data shapes; services group related RPC methods under one bounded context.
- Reserve field numbers 1-15 for your most frequently set fields, since they encode with just one byte of tag overhead.
- Use snake_case in .proto field names; code generators convert to each language's idiomatic casing automatically.
- Wrap every RPC method's parameters in a dedicated request/response message for future extensibility.
- Field numbers 19000-19999 are reserved by protobuf itself and cannot be used.
- Keep each service focused on one bounded context rather than mixing unrelated responsibilities.
Practice what you learned
1. Why should field numbers 1-15 be reserved for a message's most frequently set fields?
2. What naming convention does the .proto style guide recommend for field names?
3. Why is it recommended to wrap RPC method parameters in dedicated request/response messages instead of using bare scalar types?
4. Which range of field numbers is reserved by the Protocol Buffers implementation and cannot be used?
Was this page helpful?
You May Also Like
Protocol Buffers Explained
How Protocol Buffers serialize structured data into a compact binary format, and why gRPC uses them as its default wire format.
Generating Code from .proto
How protoc and language plugins turn a .proto file into usable client stubs and server interfaces, and how to wire that into a build.
What Is gRPC?
An introduction to gRPC, the open-source high-performance RPC framework built on HTTP/2 and Protocol Buffers for connecting services across languages.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics