Skip to content
API Design Learn/gRPC Fundamentals
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Designing Service Contracts

6 min read

You'll learn to

  • -Structure a multi-service proto definition with clear service boundaries
  • -Use protobuf's well-known types instead of reinventing common shapes like timestamps

A real gRPC system is rarely one giant service - it's usually several focused services, each owning a clear domain, calling each other where needed. Designing these boundaries well is the gRPC equivalent of the resource-modeling discipline from the REST modules, just applied to service responsibilities instead of URL paths.

Multi-Service Design

Two focused services instead of one do-everything service
service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (Order);
  rpc GetOrder (GetOrderRequest) returns (Order);
}

service InventoryService {
  rpc CheckStock (CheckStockRequest) returns (StockLevel);
  rpc ReserveStock (ReserveStockRequest) returns (ReservationResult);
}

Splitting `OrderService` and `InventoryService` rather than one combined `CommerceService` mirrors the same single-responsibility instinct from LLD Fundamentals - each service can be owned, versioned, scaled, and deployed independently by whichever team owns that domain, and a client depending only on order operations doesn't need to know inventory-specific message types exist at all.

Empty Messages: Explicit, Not Absent

A request or response with genuinely no fields still gets its own message type
service HealthService {
  rpc Check (HealthCheckRequest) returns (HealthCheckResponse);
}

message HealthCheckRequest {}   // deliberately empty - but still its own type
message HealthCheckResponse {
  bool healthy = 1;
}

Even an RPC that takes no meaningful input still gets a dedicated (if empty) request message type, rather than protobuf allowing a method to take literally no argument - this keeps every RPC's signature consistent and, importantly, gives room to add a field to that request later (say, an optional `include_details` flag on the health check) without changing the method's signature itself, the same forward-compatibility reasoning behind using input types for GraphQL mutations.

Well-Known Types: Don't Reinvent Timestamp

Using protobuf's standard Timestamp instead of a bespoke representation
import "google/protobuf/timestamp.proto";

message Order {
  int64 id = 1;
  google.protobuf.Timestamp placed_at = 2;   // standardized, not a raw int64 or string
}

Protobuf ships a set of "well-known types" for exactly these common, easy-to-get-wrong shapes - `Timestamp`, `Duration`, `Empty`, wrapper types for nullable primitives - and using them instead of reinventing a bespoke `int64 placed_at_unix_seconds = 2` field means every service that needs a timestamp represents it identically, with generated code in every language handling the conversion to/from that language's native date/time type consistently.

Reaching for a well-known type before inventing a custom representation for a common shape (time, duration, an empty request) is the protobuf equivalent of using a standard library data structure instead of hand-rolling your own - it is more likely to be correct, and every other engineer who touches the schema already understands it.

Interview Signal is part of Pro

See a real weak answer next to a real strong one for this exact topic.

Quiz is part of Pro

Test what you just read with a short quiz, and bank the XP.

Ready to Build This?

Design Service Contracts in the API Design Lab's gRPC & Event-Driven act.

ScaleDojo Logo
Initializing ScaleDojo