This episode teaches you to write gRPC contracts: the complete structure of a .proto file, modern protobuf v3 data types like enum, oneof, map, and repeated, and how to declare the four RPC patterns — unary, server streaming, client streaming, and bidirectional.

All of gRPC's power comes down to a single file: .proto. This file is the contract read by humans, the compiler, clients, and servers alike. In episode 3 you'll learn to write that contract correctly — not just a simple example, but a structure ready for real services.
We'll cover three layers of the contract: file structure (package, imports, options), protobuf v3 data types (scalar, enum, oneof, map, repeated), and service definitions with all four RPC patterns. By the end of the episode, you'll have one complete .proto file ready to be generated in episode 4.
The top of a .proto file contains three important declarations:
syntax = "proto3": selects the protobuf schema language version 3.package: the namespace that determines type names in the generated code, e.g. catalog.v1.import: includes other files, e.g. google/protobuf/timestamp.proto for time types.syntax = "proto3";
package catalog.v1;
import "google/protobuf/timestamp.proto";
option go_package = "learn-grpc/gen/catalog/v1;catalogv1";
message Product {
string id = 1;
string name = 2;
double price = 3;
}The go_package option determines the output location in Go; for other languages, the convention varies by plugin. Writing option go_package is mandatory for the Go plugin.
Every field uses a unique number within a message: string id = 1 means the field is numbered 1. This number is part of the wire format, so never change the meaning of a number already in use. Numbers 1 through 15 use only one byte — give small numbers to the fields accessed most often.
Protobuf v3 provides scalar types mapped to native language types: int32, int64, uint32, float, double, bool, string, and bytes. Note that all v3 fields are optional: default values like 0 and empty strings are used automatically when a field isn't set.
These three constructs solve most data modeling problems:
message Product {
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
STATUS_ARCHIVED = 2;
}
Status status = 4;
oneof dimension {
int32 weight_grams = 5;
int32 volume_ml = 6;
}
map<string, string> attributes = 7;
}Product has weight_grams or volume_ml.color and material.For lists of values, use repeated — the equivalent of a list or array in programming languages:
message Order {
repeated string product_ids = 1;
int32 total = 2;
}repeated string product_ids = 1 can hold zero or many IDs. For scalar types, protobuf enables packed encoding automatically so list elements are transmitted very compactly.
The heart of the contract is the service block. Four communication patterns are fully supported by protobuf:
service ProductService {
rpc GetProduct(ProductId) returns (Product);
rpc ListProducts(ProductQuery) returns (stream Product);
rpc AddBulk(stream Product) returns (BulkResult);
rpc Search(stream SearchTerm) returns (stream Product);
}Read the declarations above: rpc GetProduct(ProductId) returns (Product) is unary, ListProducts uses the stream keyword in the response, AddBulk uses stream in the request, and Search uses stream in both.
Every method needs its own request and response types. Don't reuse one message for different purposes — separate request-response pairs make schema evolution easier without breaking each other:
message ProductId {
string id = 1;
}
message ProductQuery {
string keyword = 1;
int32 limit = 2;
}
message BulkResult {
int32 added_count = 1;
}Some practices that keep your contracts healthy in the long run:
product_id) and Pascal case for messages (ProductQuery).STATUS_ACTIVE) so the generated code is unambiguous.// comments above every field and method as automatic documentation.A clean contract will make the generated code easy to read by the whole team across languages.
Key takeaways:
.proto is the single contract read by humans, the compiler, clients, and servers.package, import, and option go_package form the skeleton of a valid file.stream keyword in the right positions.In episode 4 next, you'll generate code and run your first server-client: installing plugins, the protoc command for Go, implementing a simple server, implementing a simple client, and local testing with a real request-response round trip. The .proto file you just wrote will come to life as runnable code.