Part 2: gRPC Service Design Patterns and Best Practices

The $30,000 Design Mistake

In early 2022, I designed a gRPC service for our payment processing system. I thought, "It's just like REST but with protobuf." I was wrong. Three months into production, we faced:

  • Breaking changes every week (clients constantly breaking)

  • Performance degradation (fetching nested data required 12+ RPC calls)

  • Maintenance nightmare (200+ proto files with duplicated messages)

The redesign took 6 weeks and cost approximately $30,000 in engineering time. This part contains everything I learned so you don't make the same mistakes.

Resource-Oriented Design in gRPC

The Mindset Shift

// ❌ BAD: RPC-oriented (what I did initially)
service OrderOperations {
  rpc DoCreateOrder(CreateParams) returns (OrderResult);
  rpc DoUpdateOrder(UpdateParams) returns (UpdateResult);
  rpc DoDeleteOrder(DeleteParams) returns (DeleteResult);
  rpc DoGetOrder(GetParams) returns (OrderData);
}

// βœ… GOOD: Resource-oriented (what I should have done)
service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc UpdateOrder(UpdateOrderRequest) returns (Order);
  rpc DeleteOrder(DeleteOrderRequest) returns (google.protobuf.Empty);
}

Why Resource-Oriented?

  • Consistent patterns across all services

  • Easier to understand and maintain

  • Maps well to database operations

  • Follows Google's API design guide

Standard Methods Pattern

My Production Implementation

Message Design Best Practices

1. Use Proper Naming Conventions

My Naming Rules:

  • Messages: PascalCase (CreateOrderRequest)

  • Fields: snake_case (user_id, shipping_address)

  • Enums: UPPER_SNAKE_CASE with prefix (ORDER_STATUS_PENDING)

  • Services: PascalCase with "Service" suffix (OrderService)

  • RPCs: PascalCase, verb-based (GetOrder, CreateOrder)

2. Design for Evolution

The Golden Rules I Follow:

  1. Never delete fields - Mark as deprecated instead

  2. Never reuse field numbers - Ever!

  3. Never change field types - Add new fields instead

  4. Add, don't modify - Always additive changes

3. Use Nested Messages Wisely

4. Handle Optional Fields Correctly

Production Story: I had a bug where users couldn't reset their age to 0 (baby?). The system thought "0 = not set" and ignored the update. Wrapper types fixed it.

5. Use Appropriate Data Types

Real Bug: Used float for prices. A $19.99 item became $19.989999... in the database. Customers complained. Learned my lesson.

Pagination Patterns

Cursor-Based Pagination (My Preferred Method)

TypeScript Implementation:

Why Cursor-Based?

  • Consistent results even if data changes

  • Performance doesn't degrade with deep pages

  • No "page 1000" problem

Offset-Based Pagination (Simple Use Cases)

Use When: Data is relatively static, users need to jump to specific pages.

Filtering and Searching

Structured Filtering

My Implementation (using Elasticsearch):

Error Handling Patterns

Rich Error Details

TypeScript Implementation:

Custom Error Messages

Versioning Strategies

1. Package Versioning (My Preferred)

Server Setup:

2. Field Versioning

3. Method Versioning

Performance Optimization Patterns

1. Batch Operations

Performance Impact (from my production):

  • Individual calls: 100 products = 100 RPCs = 2.5s

  • Batch call: 100 products = 1 RPC = 85ms (29x faster)

2. Field Masks

Client Usage:

3. Streaming for Large Datasets

When to Use:

  • Large result sets (1000+ items)

  • Real-time updates

  • Progressive processing

Service Organization

Microservices Boundary Design

Shared Messages

Idempotency

Server Implementation:

Why Critical: In my payment service, network issues caused duplicate charges. Idempotency keys prevented $15,000 in double charges over 6 months.

API Documentation

Key Takeaways

  1. Resource-Oriented Design: Think in resources (nouns), not operations (verbs)

  2. Evolution > Perfection: Design for change, use field masks and versioning

  3. Performance Matters: Use batch operations, streaming, and field selection

  4. Error Handling: Provide rich error details for better debugging

  5. Document Everything: Future you (and your team) will thank you

My Biggest Lesson: Spend time on design upfront. A week of design saves months of refactoring.


Next: Part 3: Building gRPC Services with TypeScript and Node.js

Series Navigation: gRPC 101 Series

Last updated