gRPC Development
This skill covers best practices for building gRPC-based services and APIs using Protocol Buffers, including service design, streaming patterns, interceptors, security, and observability.
Workflow for Building a gRPC Service
- Define the service contract — Write
.proto files with service definitions, RPC methods, and message types following the style and naming conventions below.
- Generate language stubs — Run
protoc with the appropriate language plugin (e.g., protoc-gen-go-grpc, grpcio-tools) to produce server and client code.
- Implement the server — Create handler functions for each RPC method, register them with a gRPC server, and configure TLS, interceptors, and health checks.
- Implement the client — Create a channel to the server, instantiate the generated client stub, and call RPC methods with proper deadlines and error handling.
- Add interceptors — Wire in server and client interceptors for logging, authentication, metrics, and tracing.
- Write tests — Unit-test handlers with mocked dependencies; integration-test with a real gRPC connection.
- Deploy and observe — Enable distributed tracing (OpenTelemetry), structured logging, and metrics dashboards before going to production.
Core Principles
- gRPC uses Protocol Buffers as both its Interface Definition Language (IDL) and message interchange format
- Design services around the idea of defining methods that can be called remotely with their parameters and return types
- Prioritize type safety, performance, and backward compatibility
- Leave NO todos, placeholders, or missing pieces in the implementation
Protocol Buffer Best Practices
File Organization (1-1-1 Pattern)
- Structure definitions with one top-level entity (message, enum, or extension) per .proto file
- Correspond each .proto file to a single build rule
- This promotes small, modular proto definitions
- Benefits include simplified refactoring, improved build times, and smaller binary sizes
Message Design
- Use structured messages for extensibility - Protocol Buffers supports adding fields without breaking existing clients
- Be careful to use structs in places you may want to add fields later
- Don't re-use messages across RPCs - APIs may change over time, avoid coupling separate RPC calls tightly together
- Fields should always be independent of each other - don't have one field influence the semantic meaning of another
Field Guidelines
- Use descriptive field names with underscore_separated_names
- Reserve field numbers for deleted fields to prevent future conflicts
- Use
optional for fields that may not always be present
- Consider using
oneof when users need to choose between mutually exclusive options
Enum Best Practices
- Ensure the first value is always 0
- Use an "UNSPECIFIED" default value (e.g.,
STATUS_UNSPECIFIED = 0)
- Use prefixes to avoid naming collisions (e.g.,
ORDER_STATUS_CREATED vs STATUS_PENDING)
- Reserve enum values that are removed to prevent accidental reuse
Style Guidelines
- Keep line length to 80 characters
- Prefer double quotes for strings
- Package names should be in lowercase
- Use CamelCase (with initial capital) for message names
- Use underscore_separated_names for field names
- Use CamelCase for service and RPC method names
Service Design
RPC Patterns
- Unary RPC: Client sends single request, server responds with single response
- Server Streaming: Client sends request, server responds with stream of messages
- Client Streaming: Client sends stream of messages, server responds with single response
- Bidirectional Streaming: Both sides send streams of messages
Example: Proto Definition
syntax = "proto3";
package order.v1;
option go_package = "gen/order/v1;orderv1";
// OrderService manages customer orders.
service OrderService {
// Creates a new order and returns the created resource.
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
// Streams real-time status updates for an order.
rpc WatchOrder(WatchOrderRequest) returns (stream OrderStatus);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
}
message WatchOrderRequest {
string order_id = 1;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
}
message OrderStatus {
string order_id = 1;
OrderState state = 2;
string updated_at = 3;
}
enum OrderState {
ORDER_STATE_UNSPECIFIED = 0;
ORDER_STATE_CREATED = 1;
ORDER_STATE_PROCESSING = 2;
ORDER_STATE_SHIPPED = 3;
ORDER_STATE_DELIVERED = 4;
}
Example: Go Server Implementation
package main
import (
"context"
"log"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "example.com/gen/order/v1"
)
type orderServer struct {
pb.UnimplementedOrderServiceServer
}
func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
if req.GetCustomerId() == "" {
return nil, status.Error(codes.InvalidArgument, "customer_id is required")
}
orderID := "ord-" + time.Now().Format("20060102150405")
return &pb.CreateOrderResponse{
OrderId: orderID,
Status: &pb.OrderStatus{
OrderId: orderID,
State: pb.OrderState_ORDER_STATE_CREATED,
},
}, nil
}
func (s *orderServer) WatchOrder(req *pb.WatchOrderRequest, stream pb.OrderService_WatchOrderServer) error {
for i, state := range []pb.OrderState{
pb.OrderState_ORDER_STATE_PROCESSING,
pb.OrderState_ORDER_STATE_SHIPPED,
pb.OrderState_ORDER_STATE_DELIVERED,
} {
select {
case <-stream.Context().Done():
return stream.Context().Err()
case <-time.After(time.Duration(i) * time.Second):
if err := stream.Send(&pb.OrderStatus{
OrderId: req.GetOrderId(),
State: state,
UpdatedAt: time.Now().Format(time.RFC3339),
}); err != nil {
return err
}
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer(
grpc.UnaryInterceptor(loggingUnaryInterceptor),
)
pb.RegisterOrderServiceServer(srv, &orderServer{})
log.Println("serving on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
// loggingUnaryInterceptor logs each unary RPC call.
func loggingUnaryInterceptor(
ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
API Design
- Design clear, intuitive service interfaces
- Group related methods in the same service
- Use meaningful method names that describe the action
- Document each RPC with comments describing behavior, parameters, and return values
Performance Optimization
Channel Management
- Reuse channels when working with gRPC
- Creating a gRPC channel is costly as it creates a new HTTP/2 connection
- Implement connection pooling for high-throughput scenarios
- Configure keepalive settings appropriately
Message Optimization
- Keep messages reasonably sized - large messages impact performance
- Consider streaming for large data transfers
- Use compression for bandwidth-constrained environments
- Avoid deeply nested message structures
Error Handling
Status Codes
- Use appropriate gRPC status codes (OK, INVALID_ARGUMENT, NOT_FOUND, etc.)
- Include meaningful error messages in status details
- Use rich error details for complex error scenarios
- Document expected error conditions in service definitions
Retry Logic
- Implement retry with exponential backoff for transient failures
- Use deadlines/timeouts for all RPC calls
- Handle UNAVAILABLE and RESOURCE_EXHAUSTED with retries
- Don't retry non-idempotent operations blindly
Security
Authentication
- Use TLS for transport security in production
- Implement per-RPC authentication using metadata/headers
- Support multiple authentication mechanisms (JWT, OAuth2, mTLS)
- Validate credentials on every request
Authorization
- Implement method-level access control
- Use interceptors for centralized authorization logic
- Validate all input data regardless of authentication status
- Follow the principle of least privilege
Interceptors and Middleware
Server Interceptors
- Use interceptors for cross-cutting concerns (logging, auth, metrics)
- Order interceptors carefully - execution order matters
- Keep interceptors focused on single responsibilities
- Handle errors gracefully within interceptors
Client Interceptors
- Add metadata (headers) for tracing and authentication
- Implement request/response logging
- Add automatic retry logic
- Collect client-side metrics
Testing
Unit Testing
- Mock gRPC services for isolated testing
- Test message serialization/deserialization
- Verify error handling paths
- Test interceptor logic independently
Integration Testing
- Test with real gRPC connections where possible
- Verify streaming behavior end-to-end
- Test timeout and cancellation scenarios
- Load test with realistic traffic patterns
Observability
Distributed Tracing
- Use OpenTelemetry for distributed tracing across service boundaries
- Propagate trace context in metadata
- Instrument both client and server sides
- Start spans for each RPC call
Metrics
- Track RPC latency histograms
- Monitor error rates by method and status code
- Count active connections and streams
- Alert on anomalies and SLA violations
Logging
- Use structured logging with consistent fields
- Log RPC method, duration, and status
- Include trace IDs for correlation
- Avoid logging sensitive data
Language-Specific Guidelines
Go
- Use the official
google.golang.org/grpc package
- Implement services as interface types
- Use context for cancellation and deadlines
- Leverage code generation with
protoc-gen-go-grpc
Python
- Use
grpcio and grpcio-tools packages
- Implement async services with
grpcio-aio for better concurrency
- Use type hints with generated stubs
- Handle blocking calls appropriately in async contexts
Node.js/TypeScript
- Use
@grpc/grpc-js (pure JavaScript implementation)
- Consider using
nice-grpc for better TypeScript support
- Leverage async/await patterns
- Use static codegen for type safety
1---2name: grpc-development3description: Best practices for building high-performance services with gRPC and Protocol Buffers. Use when designing RPC services, defining protobuf schemas, implementing streaming APIs, setting up gRPC interceptors, or building cross-language service communication.4---5
6# gRPC Development
7
8This skill covers best practices for building gRPC-based services and APIs using Protocol Buffers, including service design, streaming patterns, interceptors, security, and observability.
9
10## Workflow for Building a gRPC Service
11
121. **Define the service contract** — Write `.proto` files with service definitions, RPC methods, and message types following the style and naming conventions below.
132. **Generate language stubs** — Run `protoc` with the appropriate language plugin (e.g., `protoc-gen-go-grpc`, `grpcio-tools`) to produce server and client code.
143. **Implement the server** — Create handler functions for each RPC method, register them with a gRPC server, and configure TLS, interceptors, and health checks.
154. **Implement the client** — Create a channel to the server, instantiate the generated client stub, and call RPC methods with proper deadlines and error handling.
165. **Add interceptors** — Wire in server and client interceptors for logging, authentication, metrics, and tracing.
176. **Write tests** — Unit-test handlers with mocked dependencies; integration-test with a real gRPC connection.
187. **Deploy and observe** — Enable distributed tracing (OpenTelemetry), structured logging, and metrics dashboards before going to production.
19
20## Core Principles
21
22- gRPC uses Protocol Buffers as both its Interface Definition Language (IDL) and message interchange format
23- Design services around the idea of defining methods that can be called remotely with their parameters and return types
24- Prioritize type safety, performance, and backward compatibility
25- Leave NO todos, placeholders, or missing pieces in the implementation
26
27## Protocol Buffer Best Practices
28
29### File Organization (1-1-1 Pattern)
30
31- Structure definitions with one top-level entity (message, enum, or extension) per .proto file
32- Correspond each .proto file to a single build rule
33- This promotes small, modular proto definitions
34- Benefits include simplified refactoring, improved build times, and smaller binary sizes
35
36### Message Design
37
38- Use structured messages for extensibility - Protocol Buffers supports adding fields without breaking existing clients
39- Be careful to use structs in places you may want to add fields later
40- Don't re-use messages across RPCs - APIs may change over time, avoid coupling separate RPC calls tightly together
41- Fields should always be independent of each other - don't have one field influence the semantic meaning of another
42
43### Field Guidelines
44
45- Use descriptive field names with underscore_separated_names
46- Reserve field numbers for deleted fields to prevent future conflicts
47- Use `optional` for fields that may not always be present
48- Consider using `oneof` when users need to choose between mutually exclusive options
49
50### Enum Best Practices
51
52- Ensure the first value is always 0
53- Use an "UNSPECIFIED" default value (e.g., `STATUS_UNSPECIFIED = 0`)
54- Use prefixes to avoid naming collisions (e.g., `ORDER_STATUS_CREATED` vs `STATUS_PENDING`)
55- Reserve enum values that are removed to prevent accidental reuse
56
57## Style Guidelines
58
59- Keep line length to 80 characters
60- Prefer double quotes for strings
61- Package names should be in lowercase
62- Use CamelCase (with initial capital) for message names
63- Use underscore_separated_names for field names
64- Use CamelCase for service and RPC method names
65
66## Service Design
67
68### RPC Patterns
69
70- **Unary RPC**: Client sends single request, server responds with single response
71- **Server Streaming**: Client sends request, server responds with stream of messages
72- **Client Streaming**: Client sends stream of messages, server responds with single response
73- **Bidirectional Streaming**: Both sides send streams of messages
74
75### Example: Proto Definition
76
77```proto
78syntax = "proto3";
79
80package order.v1;
81
82option go_package = "gen/order/v1;orderv1";
83
84// OrderService manages customer orders.
85service OrderService {
86 // Creates a new order and returns the created resource.
87 rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
88 // Streams real-time status updates for an order.
89 rpc WatchOrder(WatchOrderRequest) returns (stream OrderStatus);
90}
91
92message CreateOrderRequest {
93 string customer_id = 1;
94 repeated OrderItem items = 2;
95}
96
97message CreateOrderResponse {
98 string order_id = 1;
99 OrderStatus status = 2;
100}
101
102message WatchOrderRequest {
103 string order_id = 1;
104}
105
106message OrderItem {
107 string product_id = 1;
108 int32 quantity = 2;
109}
110
111message OrderStatus {
112 string order_id = 1;
113 OrderState state = 2;
114 string updated_at = 3;
115}
116
117enum OrderState {
118 ORDER_STATE_UNSPECIFIED = 0;
119 ORDER_STATE_CREATED = 1;
120 ORDER_STATE_PROCESSING = 2;
121 ORDER_STATE_SHIPPED = 3;
122 ORDER_STATE_DELIVERED = 4;
123}
124```
125
126### Example: Go Server Implementation
127
128```go
129package main
130
131import (
132 "context"
133 "log"
134 "net"
135 "time"
136
137 "google.golang.org/grpc"
138 "google.golang.org/grpc/codes"
139 "google.golang.org/grpc/status"
140
141 pb "example.com/gen/order/v1"
142)
143
144type orderServer struct {
145 pb.UnimplementedOrderServiceServer
146}
147
148func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
149 if req.GetCustomerId() == "" {
150 return nil, status.Error(codes.InvalidArgument, "customer_id is required")
151 }
152 orderID := "ord-" + time.Now().Format("20060102150405")
153 return &pb.CreateOrderResponse{
154 OrderId: orderID,
155 Status: &pb.OrderStatus{
156 OrderId: orderID,
157 State: pb.OrderState_ORDER_STATE_CREATED,
158 },
159 }, nil
160}
161
162func (s *orderServer) WatchOrder(req *pb.WatchOrderRequest, stream pb.OrderService_WatchOrderServer) error {
163 for i, state := range []pb.OrderState{
164 pb.OrderState_ORDER_STATE_PROCESSING,
165 pb.OrderState_ORDER_STATE_SHIPPED,
166 pb.OrderState_ORDER_STATE_DELIVERED,
167 } {
168 select {
169 case <-stream.Context().Done():
170 return stream.Context().Err()
171 case <-time.After(time.Duration(i) * time.Second):
172 if err := stream.Send(&pb.OrderStatus{
173 OrderId: req.GetOrderId(),
174 State: state,
175 UpdatedAt: time.Now().Format(time.RFC3339),
176 }); err != nil {
177 return err
178 }
179 }
180 }
181 return nil
182}
183
184func main() {
185 lis, err := net.Listen("tcp", ":50051")
186 if err != nil {
187 log.Fatalf("failed to listen: %v", err)
188 }
189 srv := grpc.NewServer(
190 grpc.UnaryInterceptor(loggingUnaryInterceptor),
191 )
192 pb.RegisterOrderServiceServer(srv, &orderServer{})
193 log.Println("serving on :50051")
194 if err := srv.Serve(lis); err != nil {
195 log.Fatalf("failed to serve: %v", err)
196 }
197}
198
199// loggingUnaryInterceptor logs each unary RPC call.
200func loggingUnaryInterceptor(
201 ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
202) (any, error) {
203 start := time.Now()
204 resp, err := handler(ctx, req)
205 log.Printf("method=%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
206 return resp, err
207}
208```
209
210### API Design
211
212- Design clear, intuitive service interfaces
213- Group related methods in the same service
214- Use meaningful method names that describe the action
215- Document each RPC with comments describing behavior, parameters, and return values
216
217## Performance Optimization
218
219### Channel Management
220
221- Reuse channels when working with gRPC
222- Creating a gRPC channel is costly as it creates a new HTTP/2 connection
223- Implement connection pooling for high-throughput scenarios
224- Configure keepalive settings appropriately
225
226### Message Optimization
227
228- Keep messages reasonably sized - large messages impact performance
229- Consider streaming for large data transfers
230- Use compression for bandwidth-constrained environments
231- Avoid deeply nested message structures
232
233## Error Handling
234
235### Status Codes
236
237- Use appropriate gRPC status codes (OK, INVALID_ARGUMENT, NOT_FOUND, etc.)
238- Include meaningful error messages in status details
239- Use rich error details for complex error scenarios
240- Document expected error conditions in service definitions
241
242### Retry Logic
243
244- Implement retry with exponential backoff for transient failures
245- Use deadlines/timeouts for all RPC calls
246- Handle UNAVAILABLE and RESOURCE_EXHAUSTED with retries
247- Don't retry non-idempotent operations blindly
248
249## Security
250
251### Authentication
252
253- Use TLS for transport security in production
254- Implement per-RPC authentication using metadata/headers
255- Support multiple authentication mechanisms (JWT, OAuth2, mTLS)
256- Validate credentials on every request
257
258### Authorization
259
260- Implement method-level access control
261- Use interceptors for centralized authorization logic
262- Validate all input data regardless of authentication status
263- Follow the principle of least privilege
264
265## Interceptors and Middleware
266
267### Server Interceptors
268
269- Use interceptors for cross-cutting concerns (logging, auth, metrics)
270- Order interceptors carefully - execution order matters
271- Keep interceptors focused on single responsibilities
272- Handle errors gracefully within interceptors
273
274### Client Interceptors
275
276- Add metadata (headers) for tracing and authentication
277- Implement request/response logging
278- Add automatic retry logic
279- Collect client-side metrics
280
281## Testing
282
283### Unit Testing
284
285- Mock gRPC services for isolated testing
286- Test message serialization/deserialization
287- Verify error handling paths
288- Test interceptor logic independently
289
290### Integration Testing
291
292- Test with real gRPC connections where possible
293- Verify streaming behavior end-to-end
294- Test timeout and cancellation scenarios
295- Load test with realistic traffic patterns
296
297## Observability
298
299### Distributed Tracing
300
301- Use OpenTelemetry for distributed tracing across service boundaries
302- Propagate trace context in metadata
303- Instrument both client and server sides
304- Start spans for each RPC call
305
306### Metrics
307
308- Track RPC latency histograms
309- Monitor error rates by method and status code
310- Count active connections and streams
311- Alert on anomalies and SLA violations
312
313### Logging
314
315- Use structured logging with consistent fields
316- Log RPC method, duration, and status
317- Include trace IDs for correlation
318- Avoid logging sensitive data
319
320## Language-Specific Guidelines
321
322### Go
323
324- Use the official `google.golang.org/grpc` package
325- Implement services as interface types
326- Use context for cancellation and deadlines
327- Leverage code generation with `protoc-gen-go-grpc`
328
329### Python
330
331- Use `grpcio` and `grpcio-tools` packages
332- Implement async services with `grpcio-aio` for better concurrency
333- Use type hints with generated stubs
334- Handle blocking calls appropriately in async contexts
335
336### Node.js/TypeScript
337
338- Use `@grpc/grpc-js` (pure JavaScript implementation)
339- Consider using `nice-grpc` for better TypeScript support
340- Leverage async/await patterns
341- Use static codegen for type safety