Part 6: API Documentation and Versioning in gRPC

The Undocumented API Nightmare

In 2021, I inherited a gRPC microservices project with 15 services and zero documentation. Every proto file was a cryptic mess of field numbers and abbreviated names. New team members took 2-3 weeks to understand the API contracts. Integration with external teams required endless Slack messages and Zoom calls.

The final straw? A partner integration that should have taken 2 days took 3 weeks because no one knew what fields were required, what the valid values were, or how error handling worked. That project delay cost approximately $40,000 in missed revenue.

This part covers everything about documenting and versioning gRPC APIs properly—lessons learned from managing 23 production services.

Protocol Buffer Documentation

Comprehensive Proto Comments

// protos/order/v1/order.proto
syntax = "proto3";

package order.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "common/types.proto";

/// OrderService manages customer orders in the e-commerce platform.
///
/// All methods require authentication via JWT token in metadata.
/// Rate limits apply per user: 100 requests/minute for read operations,
/// 10 requests/minute for write operations.
///
/// Support: [email protected]
/// SLA: 99.9% uptime
service OrderService {
  /// Creates a new order for a customer.
  ///
  /// This operation is idempotent when using the same idempotency_key.
  /// The order will be created with status PENDING and requires
  /// subsequent payment processing.
  ///
  /// Business Rules:
  /// - User must have valid billing address
  /// - All products must be in stock
  /// - Maximum 50 items per order
  /// - Total amount must not exceed $50,000 USD
  ///
  /// Error Codes:
  /// - INVALID_ARGUMENT: Invalid request parameters or missing required fields
  /// - NOT_FOUND: Product not found
  /// - FAILED_PRECONDITION: Insufficient stock, user not eligible
  /// - ALREADY_EXISTS: Order with same idempotency key exists
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  /// - RESOURCE_EXHAUSTED: Rate limit exceeded
  ///
  /// Example:
  /// ```
  /// {
  ///   "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  ///   "user_id": "usr_123",
  ///   "items": [
  ///     {"product_id": "prod_456", "quantity": 2}
  ///   ],
  ///   "shipping_address": {
  ///     "street": "123 Main St",
  ///     "city": "San Francisco",
  ///     "state": "CA",
  ///     "postal_code": "94105",
  ///     "country": "US"
  ///   }
  /// }
  /// ```
  rpc CreateOrder(CreateOrderRequest) returns (Order);

  /// Retrieves an order by ID.
  ///
  /// Users can only retrieve their own orders unless they have
  /// the 'order:read:all' permission (admin/manager roles).
  ///
  /// Error Codes:
  /// - INVALID_ARGUMENT: Invalid order ID format
  /// - NOT_FOUND: Order not found
  /// - PERMISSION_DENIED: User doesn't have access to this order
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  ///
  /// Performance: Average 50ms, P99 150ms
  rpc GetOrder(GetOrderRequest) returns (Order);

  /// Lists orders with filtering and pagination.
  ///
  /// Returns orders sorted by created_at DESC (newest first).
  /// Maximum page size is 100 items.
  ///
  /// Filtering:
  /// - By user_id: Required for non-admin users
  /// - By status: Optional status filter
  ///
  /// Error Codes:
  /// - INVALID_ARGUMENT: Invalid parameters (page_size > 100, etc.)
  /// - PERMISSION_DENIED: Non-admin user querying other users' orders
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  ///
  /// Performance: Average 100ms, P99 300ms
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);

  /// Updates the status of an order.
  ///
  /// Requires 'order:update' permission (manager/admin only).
  ///
  /// Valid status transitions:
  /// - PENDING -> CONFIRMED, CANCELLED
  /// - CONFIRMED -> PROCESSING, CANCELLED
  /// - PROCESSING -> SHIPPED
  /// - SHIPPED -> DELIVERED
  /// - DELIVERED -> REFUNDED
  ///
  /// Error Codes:
  /// - INVALID_ARGUMENT: Invalid status value
  /// - NOT_FOUND: Order not found
  /// - FAILED_PRECONDITION: Invalid status transition
  /// - PERMISSION_DENIED: Insufficient permissions
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  rpc UpdateOrderStatus(UpdateOrderStatusRequest) returns (Order);

  /// Cancels an order.
  ///
  /// Can only cancel orders in PENDING or CONFIRMED status.
  /// Users can cancel their own orders. Admins can cancel any order.
  ///
  /// This operation:
  /// - Restores product inventory
  /// - Triggers refund if payment was processed
  /// - Sends cancellation notification to user
  ///
  /// Error Codes:
  /// - NOT_FOUND: Order not found
  /// - FAILED_PRECONDITION: Order cannot be cancelled in current status
  /// - PERMISSION_DENIED: User doesn't have permission to cancel
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  rpc CancelOrder(CancelOrderRequest) returns (Order);

  /// Streams real-time status updates for an order.
  ///
  /// Server streaming RPC that sends updates when order status changes.
  /// Stream automatically closes when order reaches terminal status
  /// (DELIVERED, CANCELLED, or REFUNDED).
  ///
  /// Typical update frequency: Every 30-60 seconds
  /// Maximum stream duration: 24 hours
  ///
  /// Error Codes:
  /// - NOT_FOUND: Order not found
  /// - PERMISSION_DENIED: User doesn't have access to this order
  /// - UNAUTHENTICATED: Missing or invalid authentication token
  rpc StreamOrderUpdates(StreamOrderUpdatesRequest) returns (stream OrderUpdate);
}

/// Represents a customer order
message Order {
  /// Unique order identifier.
  /// Format: "ord_" followed by 16 alphanumeric characters
  /// Example: "ord_a1b2c3d4e5f6g7h8"
  string id = 1;

  /// ID of the user who placed the order.
  /// Format: "usr_" followed by 16 alphanumeric characters
  /// Example: "usr_x1y2z3a4b5c6d7e8"
  string user_id = 2;

  /// Current status of the order.
  /// See OrderStatus enum for details.
  /// Default: ORDER_STATUS_PENDING
  OrderStatus status = 3;

  /// List of items in the order.
  /// Minimum: 1 item
  /// Maximum: 50 items
  repeated OrderItem items = 4;

  /// Shipping address for the order.
  /// Required field.
  common.v1.Address shipping_address = 5;

  /// Subtotal before tax and shipping.
  /// Sum of all item total_price values.
  common.v1.Money subtotal = 6;

  /// Tax amount.
  /// Calculated based on shipping address and local tax rates.
  common.v1.Money tax = 7;

  /// Shipping cost.
  /// Calculated based on weight, dimensions, and shipping method.
  common.v1.Money shipping_cost = 8;

  /// Total amount (subtotal + tax + shipping_cost).
  /// This is the amount charged to the customer.
  common.v1.Money total = 9;

  /// Timestamp when the order was created.
  /// Immutable after creation.
  google.protobuf.Timestamp created_at = 10;

  /// Timestamp when the order was last updated.
  /// Updated whenever any field changes.
  google.protobuf.Timestamp updated_at = 11;
}

/// Represents a single item within an order
message OrderItem {
  /// Unique item identifier (within the order).
  /// Generated by the system.
  string id = 1;

  /// ID of the product.
  /// Must reference a valid product in the catalog.
  string product_id = 2;

  /// Name of the product at time of order.
  /// Snapshot of product name (may differ from current product name).
  string product_name = 3;

  /// Quantity ordered.
  /// Minimum: 1
  /// Maximum: 100 per item
  int32 quantity = 4;

  /// Unit price at time of order.
  /// Snapshot of product price (may differ from current price).
  common.v1.Money unit_price = 5;

  /// Total price for this item (unit_price * quantity).
  /// Calculated field.
  common.v1.Money total_price = 6;
}

/// Order status values
enum OrderStatus {
  /// Default value. Should never be used.
  /// If you see this, there's a bug.
  ORDER_STATUS_UNSPECIFIED = 0;

  /// Order created but not yet confirmed.
  /// Awaiting payment or manual review.
  ORDER_STATUS_PENDING = 1;

  /// Order confirmed and payment received.
  /// Ready for processing.
  ORDER_STATUS_CONFIRMED = 2;

  /// Order is being prepared/packaged.
  /// Inventory has been reserved.
  ORDER_STATUS_PROCESSING = 3;

  /// Order has been shipped.
  /// Tracking information available.
  ORDER_STATUS_SHIPPED = 4;

  /// Order delivered to customer.
  /// Terminal status (unless refunded).
  ORDER_STATUS_DELIVERED = 5;

  /// Order cancelled by user or system.
  /// Inventory restored, refund issued if applicable.
  /// Terminal status.
  ORDER_STATUS_CANCELLED = 6;

  /// Order refunded after delivery.
  /// Payment reversed to customer.
  /// Terminal status.
  ORDER_STATUS_REFUNDED = 7;
}

/// Request to create a new order
message CreateOrderRequest {
  /// Idempotency key to prevent duplicate orders.
  /// Use a UUID v4 for uniqueness.
  /// Required field.
  /// Max length: 128 characters
  ///
  /// If a request with the same idempotency_key is received within 24 hours,
  /// the previously created order will be returned without creating a new one.
  ///
  /// Example: "550e8400-e29b-41d4-a716-446655440000"
  string idempotency_key = 1;

  /// ID of the user placing the order.
  /// Must match authenticated user unless caller has admin role.
  /// Required field.
  string user_id = 2;

  /// Items to include in the order.
  /// Required field.
  /// Minimum: 1 item
  /// Maximum: 50 items
  repeated CreateOrderItem items = 3;

  /// Shipping address for the order.
  /// Required field.
  /// Must be a valid, deliverable address.
  common.v1.Address shipping_address = 4;
}

/// Item to add to a new order
message CreateOrderItem {
  /// ID of the product to order.
  /// Must reference a valid, active product.
  /// Required field.
  string product_id = 1;

  /// Quantity to order.
  /// Required field.
  /// Minimum: 1
  /// Maximum: 100
  ///
  /// Quantity must not exceed available stock.
  int32 quantity = 2;
}

// ... Additional messages with similar documentation

Documentation Best Practices

  1. Service-level comments: Purpose, authentication, rate limits, SLA

  2. Method-level comments: Behavior, business rules, error codes, examples

  3. Message-level comments: Purpose, constraints, relationships

  4. Field-level comments: Format, validation rules, examples

  5. Enum-level comments: Each value explained

Generating API Documentation

Using protoc-gen-doc

Custom Documentation Template

Automated Documentation Generation

API Versioning Strategies

Server Supporting Multiple Versions

Client Choosing Version

2. Field Versioning (Backward Compatible)

3. Method Versioning

Migration Strategies

Gradual Migration Pattern

Deprecation Headers

Breaking Change Checklist

Safe Changes (Backward Compatible)

Adding new fields to messages

Adding new methods to services

Adding new enum values

Making required fields optional

Breaking Changes (Require New Version)

Deleting fields

Changing field types

Reusing field numbers

Renaming fields or messages

Changing method signatures

Version Sunset Policy

Changelog and Release Notes

CHANGELOG.md

Documentation Website

Using Docusaurus

Key Takeaways

  1. Document Everything: Comments in proto files are your API contract

  2. Version Properly: Use package versioning for breaking changes

  3. Support Multiple Versions: Run v1 and v2 simultaneously during migration

  4. Communicate Changes: Changelog, deprecation warnings, migration guides

  5. Plan Sunsets: Give clients 6-12 months to migrate

  6. Never Break Silently: Use proper versioning, not silent breaking changes

  7. Automate Docs: Generate documentation from proto files

My $40,000 Lesson: Good documentation pays for itself in reduced integration time and support costs.


Next: Part 7: Testing, Performance, and Production Deployment

Series Navigation: gRPC 101 Series

Last updated