AlgoMaster Logo

gRPC Service Design

Medium Priority12 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

A sequence of successful remote calls can still produce an incorrect business result. Suppose checkout reads that three units are available and then asks inventory to subtract two. Another checkout can read the same number before that subtraction finishes. The client may encode every request correctly and deliver it successfully, yet the system can still promise more stock than it has.

A useful gRPC interface exposes operations with clear business guarantees.

This chapter explains how to choose service boundaries, define methods and messages, and document the behavior that generated code cannot express. A temporary stock-reservation service provides the running example.

1. Service Boundaries

A gRPC service is a named group of remotely callable methods. Its definition is an API boundary, not a deployment instruction. Several service definitions can run in one server process, and a service implementation can use several internal components.

Group methods around a coherent responsibility that clients understand. For inventory reservations, that responsibility is creating, inspecting, and ending temporary stock holds. It does not include charging a card or writing an order confirmation email.

A service per database table tends to expose storage structure. A single service for every business operation tends to collect unrelated responsibilities. Choose a boundary based on ownership of behavior and rules that must remain true.

For this example, the inventory subsystem owns the rule that active holds cannot exceed the stock available for reservation. Checkout requests a hold; inventory decides whether enough stock is available.

The diagram separates the public contract from implementation details:

Checkout does not need to know how inventory stores hold records and stock counters. The reservation implementation can change those details while preserving the external guarantee. Payment remains a separate responsibility; creating a hold does not make checkout and payment one distributed transaction.

2. Method Intent and Granularity

A method should represent a useful unit of work. The intentionally flawed design below exposes the steps of a reservation as separate calls:

The client must coordinate concurrency and recover from partial completion. If the second call succeeds and the third fails, stock has changed without a reservation record. Making each call faster does not fix that contract.

A method such as ReserveStock lets the service check availability and create the hold as one atomic business operation. Atomic here means that the stock allocation and reservation creation take effect together, or neither does. This is an implementation obligation created by the API contract; gRPC does not provide the transaction automatically.

Prefer recognizable names such as GetReservation, ReserveStock, and ReleaseReservation over Execute or Process. Use familiar Get, List, Create, Update, and Delete names where their meaning fits. Use a domain action when the operation has rules that generic field editing would hide.

For example, releasing a reservation returns held stock and changes its lifecycle state. A generic UpdateReservation that accepts arbitrary state changes could accidentally let callers reactivate an expired hold without checking stock.

These are design recommendations, not a required gRPC method vocabulary. gRPC can support resource-oriented contracts as well as action-oriented contracts. Google API design guidelines describe one established resource-oriented convention; teams can adopt such a convention deliberately without treating it as a protocol requirement.

Method size also has a limit. A CompleteCheckout RPC that reserves inventory, charges payment, sends email, and arranges shipping needs an explicit workflow contract for partial failures. Combining unrelated work behind one method name does not make it atomic.

3. A Reservation Contract

Assume an authenticated internal caller operates within one tenant, an organizational boundary used to isolate data and permissions. The server derives that tenant from verified caller identity. The server resolves product and warehouse identifiers within that tenant.

The service creates a hold for one product in one warehouse. Each successful hold lasts ten minutes from creation unless the caller releases it earlier. This example covers temporary holds only; purchasing the held stock would require an additional, explicitly designed operation.

Here is a complete schema, suitable for a file named inventory_reservation.proto:

Service, method, and message names use TitleCase, and fields use lower snake case, following the Protobuf style conventions. inventory.v1 is this API's package choice. gRPC does not require that particular versioning scheme.

Each method has its own request and response message even when two messages initially have the same shape. This gives their contracts room to develop independently. A future option for releasing a hold should not silently become an input to reading one.

The shared Reservation message represents the same business object across responses. The server sets its identifier, product, warehouse, quantity, and expiry at creation and never changes them; its state can change. All successful response envelopes contain a populated reservation. The application must enforce this rule; the message declaration alone does not.

A dedicated response envelope can later carry operation-specific information without changing the RPC's response type. Returning Reservation directly is also a valid choice. Choose a consistent convention, and avoid adding empty layers that serve no clear purpose.

Separate inputs from server-owned output. ReserveStockRequest contains the caller's desired quantity, while the server chooses the reservation identifier, state, and expiry. Accepting the full output object as a creation request would invite ambiguity about which values the caller controls.

4. Behavioral Guarantees

A .proto file expresses structure, but a client also needs the meaning of success, failure, and repetition. Document those decisions beside the method definition or in maintained API documentation.

For this service, successful ReserveStock means the service has allocated the requested units to one durable hold. A successful GetReservation returns the current state at its read point. The returned record can change afterward, so reading HELD is not a promise that it will remain held indefinitely.

The reservation lifecycle is intentionally small:

Both terminal states mean the hold no longer consumes stock. Expiry is effective at expires_at according to server time, even if physical cleanup runs later. Reads and allocation checks must respect that logical expiry. If release races with expiry, the server returns the terminal state it establishes under that rule and returns the stock at most once.

The service retains reservation records for at least 24 hours after they become terminal. During that period, callers can inspect them and repeat release safely. After retention ends, a lookup or release may return NOT_FOUND. Record retention is part of the contract when recovery depends on reading a past operation.

The example uses these outcomes:

ConditionObservable result
Valid reserve request with sufficient stockPopulated reservation in HELD, with OK
Missing identifier, malformed request ID, or quantity outside 1–100INVALID_ARGUMENT; no hold created
Missing or invalid credentialsUNAUTHENTICATED
Caller lacks permission for the target warehouse or operationPERMISSION_DENIED
Authorized target product, warehouse, or reservation does not existNOT_FOUND
Insufficient available stock for the requested quantityFAILED_PRECONDITION; no partial hold
Release of a held reservation before expiryReservation in RELEASED, with OK
Release of an already released or expired reservationExisting terminal state, with OK

These mappings are choices for this API using standard gRPC statuses. Authorization takes place before exposing protected resource existence. Do not report an internal storage failure as insufficient stock or an unknown identifier.

For example, a caller can request two units using the following Protobuf text-format message. These examples show logical messages, not an HTTP exchange:

If the server commits the reservation at Unix time 1788600600, it returns gRPC status OK and a body such as:

The expiry is exactly 600 seconds after creation. With quantity: 0, the call instead returns INVALID_ARGUMENT and no successful response body. With valid inputs but no permission to reserve in that warehouse, it returns PERMISSION_DENIED and creates no hold.

A single-item reservation is all-or-nothing. Two calls reserving different products are two independent operations. If your application needs cart-wide atomic reservation, design that capability explicitly rather than letting callers infer it from a sequence of successful single-item calls.

5. Retries and Lost Responses

For a state-changing method, define what happens when the client cannot tell whether the first call succeeded. A field named request_id is useful only when the server implements a corresponding duplicate-request policy.

In this example, the client generates one UUID per logical reservation attempt and reuses it when retrying that attempt. The server scopes it by tenant, authenticated client identity, and method. Identity here means the stable client principal, not a particular access-token string.

Within 24 hours after a successful creation, an identical request returns the original creation response without allocating more stock. Reusing the same request ID with different product, warehouse, or quantity values returns INVALID_ARGUMENT. The server coordinates concurrent duplicates so that only one successful creation occurs, and records the success outcome atomically with the hold.

If the server rejects a request before committing a hold, it does not reserve the request ID under this policy. The server can therefore evaluate a corrected or retried request again. Clients must not assume that a transient failure means the server committed no hold.

The diagram shows why the duplicate rule belongs in the method contract:

The replay is a historical creation result. It can say HELD even if the reservation has since expired or the caller has released it. Its original expires_at remains unchanged. A client recovering from an uncertain call uses the returned identifier with GetReservation to read current state; it does not treat replay as a renewed hold.

After the 24-hour duplicate-detection window, the service may treat the same request ID as a new attempt. Clients must stop automatic retries before that window ends. A new request ID means a new attempt and can create another hold, even if the product and quantity are unchanged.

ReleaseReservation instead has a naturally repeatable state transition: ensuring a known reservation no longer holds stock. A reservation that an earlier GetReservation returned as HELD may appear as RELEASED or EXPIRED in the release response, but repeating release cannot return the units twice.

These rules do not configure gRPC retries. Clients still need bounded waiting and a retry policy appropriate to the operation. A deadline or cancellation does not undo a hold the server has already committed.

6. Message Data and Call Context

Keep business inputs visible in the typed request. Product, warehouse, quantity, and the business duplicate-request identifier belong in this example's messages. A caller should be able to understand the operation without discovering hidden metadata keys that change its meaning.

Call metadata can carry information that applies across operations, such as authentication and tracing. A tracing identifier links diagnostic records; it is not automatically a substitute for the request ID that defines one reservation attempt.

Use transport security and verified credentials to establish identity. If callers need to select a tenant explicitly, expose that selection consistently and check it against their permissions. A tenant identifier the caller supplies is a claim to validate, not proof of access.

Permission rules must also follow references. GetReservation and ReleaseReservation accept an identifier rather than a warehouse, but the server still resolves the reservation within the caller's tenant and checks access to its warehouse. Possession of an opaque identifier does not grant permission.

The schema's constraints are intentionally limited. The implementation must validate required identifiers, quantity presence and range, and identifier lengths. For this example, accept non-empty product, warehouse, and reservation identifiers up to 128 ASCII characters; request IDs use canonical UUID text. These limits are service policy, not Protobuf defaults.

7. Bounded Operations and Evolution

Design the amount of work as deliberately as the message shape. This contract reserves one product per call and limits quantity to 100. That keeps its all-or-nothing promise small enough to reason about.

If clients need reservation browsing, introduce a bounded list operation with a page size and continuation token. Specify ordering and what changes between pages can do to the result. A repeated Reservation response does not by itself bound the work or promise a consistent snapshot.

Similarly, a bulk method needs a limit on items and a declared atomicity policy. Returning item-level outcomes is useful only when callers know whether successful items remain committed after another item fails. A streaming method is another explicit contract choice, not an automatic solution to an oversized collection.

For editable resources, define which fields may change and how omitted values behave. A field mask can identify fields selected for update, but lifecycle actions such as releasing stock should retain their domain rules. A general update method should not bypass those rules.

Review evolution from a caller's perspective. Adding a new RPC can be compatible with existing callers, but new clients cannot assume every deployed server supports it. Renaming a package, service, or method changes the remote method identity and requires a migration strategy.

Behavioral changes can be equally disruptive. Changing a hold from ten minutes to thirty seconds, reducing the duplicate-detection window, or letting release return success before the service actually frees stock can break clients without changing a single field number.

Before publishing this contract, review concrete scenarios: competing reservations for the last units, a lost creation response, a repeated release, an expired hold, and a caller targeting another tenant's identifier. These cases reveal whether the interface offers a coherent guarantee. Compiling the schema checks its structure; implementation and contract tests must establish the promised behavior.

Summary

A well-designed gRPC service groups related responsibilities and exposes operations that match client intent. Request and response types should make ownership clear, while the implementation preserves the business rules behind each method.

Define success, state transitions, duplicate handling, authorization, limits, and retention as part of the contract. Generated code makes methods callable; clear semantics make them safe to depend on as services fail, recover, and evolve.