gRPC Patterns - Deep Dive
Comprehensive gRPC service design, streaming patterns, error handling, interceptors, and production deployment strategies.
Protocol Buffers (Protobuf)
Basic Message Definition
syntax = "proto3";
package users.v1;
// Import well-known types
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// User message
message User {
string id = 1;
string email = 2;
string name = 3;
UserRole role = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
// Enum for user roles
enum UserRole {
USER_ROLE_UNSPECIFIED = 0; // Required first value
USER_ROLE_USER = 1;
USER_ROLE_ADMIN = 2;
USER_ROLE_MODERATOR = 3;
}
Field Numbering Best Practices
✅ Good: Strategic numbering
message User {
// 1-15: Single-byte encoding (most common fields)
string id = 1;
string email = 2;
string name = 3;
// 16-2047: Two-byte encoding (less common fields)
string bio = 16;
string website = 17;
// 19000-19999: Reserved range (do not use)
// 20000+: Multi-byte encoding (rare fields)
}
❌ Bad: Random numbering
message User {
string id = 100; // Wastes encoding space
string email = 3;
string name = 15000; // Very inefficient
}
Nested Messages
message User {
string id = 1;
string email = 2;
Profile profile = 3;
message Profile {
string bio = 1;
string avatar_url = 2;
Address address = 3;
message Address {
string street = 1;
string city = 2;
string country = 3;
string postal_code = 4;
}
}
}
Repeated Fields (Arrays)
message User {
string id = 1;
repeated string tags = 2; // Array of strings
repeated Role roles = 3; // Array of enums
repeated Address addresses = 4; // Array of messages
}
Maps
message User {
string id = 1;
map<string, string> metadata = 2; // String map
map<string, int32> settings = 3; // Mixed types
map<string, Address> addresses = 4; // Complex values
}
Oneofs (Union Types)
message SearchRequest {
string query = 1;
oneof filter {
UserFilter user_filter = 2;
PostFilter post_filter = 3;
CommentFilter comment_filter = 4;
}
}
message UserFilter {
UserRole role = 1;
bool is_active = 2;
}
Reserved Fields
message User {
reserved 4, 5, 6; // Reserved field numbers
reserved "old_field", "deprecated"; // Reserved field names
string id = 1;
string email = 2;
string name = 3;
// Fields 4-6 cannot be reused
string new_field = 7;
}
Service Definition
Unary RPC (Request/Response)
service UserService {
rpc GetUser(GetUserRequest) returns (User) {}
rpc CreateUser(CreateUserRequest) returns (User) {}
rpc UpdateUser(UpdateUserRequest) returns (User) {}
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) {}
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string email = 1;
string name = 2;
UserRole role = 3;
}
message UpdateUserRequest {
string id = 1;
optional string email = 2;
optional string name = 3;
optional UserRole role = 4;
}
message DeleteUserRequest {
string id = 1;
}
Server Streaming RPC
Server sends multiple messages in response to single client request:
service UserService {
// Stream all users
rpc ListUsers(ListUsersRequest) returns (stream User) {}
// Stream user events
rpc WatchUser(WatchUserRequest) returns (stream UserEvent) {}
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
UserRole role = 3;
}
message WatchUserRequest {
string user_id = 1;
}
message UserEvent {
string event_id = 1;
EventType type = 2;
User user = 3;
google.protobuf.Timestamp timestamp = 4;
}
enum EventType {
EVENT_TYPE_UNSPECIFIED = 0;
EVENT_TYPE_CREATED = 1;
EVENT_TYPE_UPDATED = 2;
EVENT_TYPE_DELETED = 3;
}
Client Streaming RPC
Client sends multiple messages, server responds once:
service UserService {
// Bulk create users
rpc BulkCreateUsers(stream CreateUserRequest) returns (BulkCreateUsersResponse) {}
// Upload user data
rpc UploadUserData(stream UserDataChunk) returns (UploadResponse) {}
}
message BulkCreateUsersResponse {
int32 created_count = 1;
repeated User users = 2;
repeated Error errors = 3;
}
message UserDataChunk {
bytes data = 1;
int32 chunk_number = 2;
}
message UploadResponse {
int64 bytes_received = 1;
string file_id = 2;
}
Bidirectional Streaming RPC
Both client and server send multiple messages:
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage) {}
}
message ChatMessage {
string id = 1;
string user_id = 2;
string text = 3;
google.protobuf.Timestamp sent_at = 4;
}
Server Implementation
Go Server
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "myapp/proto/users/v1"
)
type server struct {
pb.UnimplementedUserServiceServer
db *Database
}
// Unary RPC
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "user ID is required")
}
user, err := s.db.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "database error")
}
return &pb.User{
Id: user.ID,
Email: user.Email,
Name: user.Name,
Role: pb.UserRole(user.Role),
}, nil
}
// Server streaming RPC
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
users, err := s.db.ListUsers(stream.Context(), req)
if err != nil {
return status.Error(codes.Internal, "failed to fetch users")
}
for _, user := range users {
if err := stream.Send(&pb.User{
Id: user.ID,
Email: user.Email,
Name: user.Name,
}); err != nil {
return status.Error(codes.Internal, "failed to send user")
}
}
return nil
}
// Client streaming RPC
func (s *server) BulkCreateUsers(stream pb.UserService_BulkCreateUsersServer) error {
var users []*pb.User
var errors []*pb.Error
for {
req, err := stream.Recv()
if err == io.EOF {
// Client finished sending
return stream.SendAndClose(&pb.BulkCreateUsersResponse{
CreatedCount: int32(len(users)),
Users: users,
Errors: errors,
})
}
if err != nil {
return status.Error(codes.Internal, "failed to receive request")
}
user, err := s.db.CreateUser(stream.Context(), req)
if err != nil {
errors = append(errors, &pb.Error{
Message: err.Error(),
Field: "email",
})
continue
}
users = append(users, user)
}
}
// Bidirectional streaming RPC
func (s *server) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return status.Error(codes.Internal, "failed to receive message")
}
// Process message
response := &pb.ChatMessage{
Id: generateID(),
UserId: "bot",
Text: fmt.Sprintf("Echo: %s", msg.Text),
SentAt: timestamppb.Now(),
}
if err := stream.Send(response); err != nil {
return status.Error(codes.Internal, "failed to send message")
}
}
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{db: newDatabase()})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Node.js/TypeScript Server
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { UserServiceHandlers } from './proto/users/v1/user_service';
const packageDefinition = protoLoader.loadSync('proto/users/v1/user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto = grpc.loadPackageDefinition(packageDefinition).users.v1;
const server = new grpc.Server();
const userService: UserServiceHandlers = {
// Unary RPC
getUser: async (call, callback) => {
const { id } = call.request;
if (!id) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: 'User ID is required',
});
}
try {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'User not found',
});
}
callback(null, {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
});
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: 'Database error',
});
}
},
// Server streaming RPC
listUsers: async (call) => {
const users = await db.users.findMany();
for (const user of users) {
call.write({
id: user.id,
email: user.email,
name: user.name,
});
}
call.end();
},
// Client streaming RPC
bulkCreateUsers: async (call, callback) => {
const users: any[] = [];
const errors: any[] = [];
call.on('data', async (request) => {
try {
const user = await db.users.create({ data: request });
users.push(user);
} catch (error) {
errors.push({ message: error.message, field: 'email' });
}
});
call.on('end', () => {
callback(null, {
created_count: users.length,
users,
errors,
});
});
call.on('error', (error) => {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
});
},
// Bidirectional streaming RPC
chat: (call) => {
call.on('data', (message) => {
// Echo message back
call.write({
id: generateId(),
user_id: 'bot',
text: `Echo: ${message.text}`,
sent_at: new Date(),
});
});
call.on('end', () => {
call.end();
});
},
};
server.addService(userProto.UserService.service, userService);
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
console.error('Failed to bind server:', err);
return;
}
console.log(`Server running on port ${port}`);
server.start();
}
);
Client Implementation
Go Client
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "myapp/proto/users/v1"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Unary call
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User: %v", user)
// Server streaming call
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 10})
if err != nil {
log.Fatalf("could not list users: %v", err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("error receiving: %v", err)
}
log.Printf("User: %v", user)
}
// Client streaming call
bulkStream, err := client.BulkCreateUsers(ctx)
if err != nil {
log.Fatalf("could not create bulk stream: %v", err)
}
users := []*pb.CreateUserRequest{
{Email: "alice@example.com", Name: "Alice"},
{Email: "bob@example.com", Name: "Bob"},
}
for _, req := range users {
if err := bulkStream.Send(req); err != nil {
log.Fatalf("failed to send: %v", err)
}
}
response, err := bulkStream.CloseAndRecv()
if err != nil {
log.Fatalf("failed to receive response: %v", err)
}
log.Printf("Created %d users", response.CreatedCount)
}
TypeScript Client
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('proto/users/v1/user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).users.v1;
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call
client.getUser({ id: '123' }, (error, response) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('User:', response);
});
// Promise wrapper for unary calls
function getUserAsync(id: string): Promise<any> {
return new Promise((resolve, reject) => {
client.getUser({ id }, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
}
// Server streaming call
const stream = client.listUsers({ page_size: 10 });
stream.on('data', (user) => {
console.log('User:', user);
});
stream.on('end', () => {
console.log('Stream ended');
});
stream.on('error', (error) => {
console.error('Stream error:', error);
});
// Client streaming call
const bulkStream = client.bulkCreateUsers((error, response) => {
if (error) {
console.error('Error:', error);
return;
}
console.log(`Created ${response.created_count} users`);
});
bulkStream.write({ email: 'alice@example.com', name: 'Alice' });
bulkStream.write({ email: 'bob@example.com', name: 'Bob' });
bulkStream.end();
Error Handling
gRPC Status Codes
import "google.golang.org/grpc/codes"
// codes.OK - Success
// codes.Canceled - Operation canceled
// codes.Unknown - Unknown error
// codes.InvalidArgument - Invalid client input
// codes.DeadlineExceeded - Timeout
// codes.NotFound - Resource not found
// codes.AlreadyExists - Resource already exists
// codes.PermissionDenied - No permission
// codes.ResourceExhausted - Rate limit, quota
// codes.FailedPrecondition - System state invalid
// codes.Aborted - Concurrency conflict
// codes.OutOfRange - Out of valid range
// codes.Unimplemented - Not implemented
// codes.Internal - Internal server error
// codes.Unavailable - Service unavailable
// codes.DataLoss - Data corruption
// codes.Unauthenticated - Invalid credentials
Rich Error Details
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
// Validation errors
if req.Email == "" || !strings.Contains(req.Email, "@") {
st := status.New(codes.InvalidArgument, "invalid email")
br := &errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{
Field: "email",
Description: "email must be valid format",
},
},
}
st, _ = st.WithDetails(br)
return nil, st.Err()
}
// Quota/rate limit
if !s.checkQuota(ctx) {
st := status.New(codes.ResourceExhausted, "quota exceeded")
qi := &errdetails.QuotaFailure{
Violations: []*errdetails.QuotaFailure_Violation{
{
Subject: "user:" + getUserID(ctx),
Description: "API quota exceeded. Try again in 60 seconds",
},
},
}
st, _ = st.WithDetails(qi)
return nil, st.Err()
}
return user, nil
}
Client error handling:
user, err := client.GetUser(ctx, req)
if err != nil {
st := status.Convert(err)
log.Printf("Error code: %s", st.Code())
log.Printf("Error message: %s", st.Message())
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest:
for _, violation := range t.FieldViolations {
log.Printf("Field %s: %s", violation.Field, violation.Description)
}
case *errdetails.QuotaFailure:
for _, violation := range t.Violations {
log.Printf("Quota: %s", violation.Description)
}
}
}
}
Interceptors (Middleware)
Server Interceptor (Go)
import (
"context"
"log"
"time"
"google.golang.org/grpc"
)
// Unary interceptor
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Call handler
resp, err := handler(ctx, req)
duration := time.Since(start)
log.Printf("Method: %s, Duration: %v, Error: %v", info.FullMethod, duration, err)
return resp, err
}
// Stream interceptor
func streamLoggingInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
err := handler(srv, ss)
duration := time.Since(start)
log.Printf("Stream: %s, Duration: %v, Error: %v", info.FullMethod, duration, err)
return err
}
// Register interceptors
s := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
grpc.StreamInterceptor(streamLoggingInterceptor),
)
Authentication Interceptor
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Extract metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
// Check authorization header
tokens := md["authorization"]
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
token := tokens[0]
userID, err := validateToken(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// Add user to context
ctx = context.WithValue(ctx, "userID", userID)
return handler(ctx, req)
}
Client Interceptor (Go)
func clientLoggingInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf("Method: %s, Duration: %v", method, time.Since(start))
return err
}
// Use interceptor
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithUnaryInterceptor(clientLoggingInterceptor),
)
Metadata (Headers)
Server: Read Metadata
import "google.golang.org/grpc/metadata"
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.InvalidArgument, "missing metadata")
}
// Get header values
tokens := md["authorization"]
userAgent := md["user-agent"]
log.Printf("Authorization: %v", tokens)
log.Printf("User-Agent: %v", userAgent)
return user, nil
}
Server: Send Metadata
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// Send header
header := metadata.Pairs("x-request-id", generateID())
grpc.SendHeader(ctx, header)
// Send trailer
trailer := metadata.Pairs("x-response-time", "123ms")
grpc.SetTrailer(ctx, trailer)
return user, nil
}
Client: Send Metadata
func main() {
ctx := context.Background()
// Add metadata to context
md := metadata.New(map[string]string{
"authorization": "Bearer token123",
"x-request-id": generateID(),
})
ctx = metadata.NewOutgoingContext(ctx, md)
// Make call with metadata
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
}
Client: Receive Metadata
var header, trailer metadata.MD
user, err := client.GetUser(
ctx,
req,
grpc.Header(&header),
grpc.Trailer(&trailer),
)
if err == nil {
log.Printf("Header: %v", header)
log.Printf("Trailer: %v", trailer)
}
Performance Optimization
Connection Pooling
// Client-side connection pool
var (
conn *grpc.ClientConn
connOnce sync.Once
)
func getConnection() *grpc.ClientConn {
connOnce.Do(func() {
var err error
conn, err = grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB
grpc.MaxCallSendMsgSize(10*1024*1024),
),
)
if err != nil {
log.Fatalf("Failed to dial: %v", err)
}
})
return conn
}
Keep-Alive Settings
// Server keep-alive
s := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 10 * time.Second, // Ping every 10s if no activity
Timeout: 3 * time.Second, // Wait 3s for pong
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // Min time between pings
PermitWithoutStream: true, // Allow pings when no streams
}),
)
// Client keep-alive
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
PermitWithoutStream: true,
}),
)
Compression
// Enable gzip compression
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")),
)
// Per-call compression
user, err := client.GetUser(
ctx,
req,
grpc.UseCompressor("gzip"),
)
TLS/SSL Security
Server TLS
import "google.golang.org/grpc/credentials"
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatalf("Failed to load TLS: %v", err)
}
s := grpc.NewServer(grpc.Creds(creds))
Client TLS
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
if err != nil {
log.Fatalf("Failed to load TLS: %v", err)
}
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(creds),
)
Mutual TLS (mTLS)
// Server
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("ca.crt")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{cert},
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(creds))
Health Checking
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
Implementation:
import "google.golang.org/grpc/health/grpc_health_v1"
healthServer := health.NewServer()
healthServer.SetServingStatus("users.v1.UserService", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(s, healthServer)
Testing
Unit Testing
import (
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestGetUser(t *testing.T) {
mockDB := &MockDatabase{
users: map[string]*User{
"123": {ID: "123", Email: "test@example.com"},
},
}
srv := &server{db: mockDB}
user, err := srv.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Email != "test@example.com" {
t.Errorf("expected test@example.com, got %s", user.Email)
}
// Test not found
_, err = srv.GetUser(context.Background(), &pb.GetUserRequest{Id: "999"})
if status.Code(err) != codes.NotFound {
t.Errorf("expected NotFound, got %v", status.Code(err))
}
}
Integration Testing
func TestUserService(t *testing.T) {
// Start test server
lis := bufconn.Listen(1024 * 1024)
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{db: testDB})
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("Server exited with error: %v", err)
}
}()
defer s.Stop()
// Create test client
conn, err := grpc.DialContext(
context.Background(),
"bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithInsecure(),
)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// Test GetUser
user, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.Id != "123" {
t.Errorf("expected ID 123, got %s", user.Id)
}
}
Best Practices Summary
✅ Use proto3: Modern syntax, better performance
✅ Version services: Use package versioning (users.v1)
✅ Reserve field numbers: Protect against breaking changes
✅ Use well-known types: Timestamp, Duration, Empty, Any
✅ Implement health checks: For load balancers, Kubernetes
✅ Enable TLS: Encrypt traffic in production
✅ Add interceptors: Logging, auth, metrics
✅ Use keep-alive: Maintain long-lived connections
✅ Stream large datasets: Avoid large unary responses
✅ Handle errors properly: Use correct status codes with details
❌ Don't reuse field numbers: Reserved fields protect from bugs ❌ Don't use HTTP/JSON for gRPC: Use binary Protobuf ❌ Don't ignore deadlines: Always set request timeouts ❌ Don't skip error details: Provide actionable error info ❌ Don't run without TLS: Production must use encryption ❌ Don't forget connection pooling: Reuse connections