The gRPC decision record: why the transport model you chose determines your inter-service schema evolution surface and your browser client compatibility ceiling
gRPC decisions are made during the microservices extraction session or the inter-service communication design session when the team needs a contract between two services that will carry high-volume or latency-sensitive traffic. The AI session that produces the gRPC decision is practical and tightly scoped: it compares gRPC's performance characteristics against REST/JSON, notes that Protocol Buffers provide compile-time type safety across language boundaries and a substantially smaller wire format for structured data, and evaluates the streaming options for the specific use case at hand. The session produces a clear decision — adopt gRPC for the internal service communication layer, define the proto file for the service's methods and message types, and generate the client and server stubs in the team's language of choice. The decision is immediate, correct for its stated purpose, and allows the feature work to proceed without the manual client maintenance overhead that REST API evolution has been generating.
What the AI session does not produce is the gRPC system's second half. The session answers "which transport protocol?" and defines the first proto schema. It does not ask: who owns the proto definitions when those definitions are shared across multiple services owned by multiple teams? What constitutes a breaking versus a non-breaking change to a proto schema, and what is the deprecation protocol for removing a field that six services depend on and that is owned by a team other than your own? What streaming model — unary, server-streaming, client-streaming, or bidirectional — is appropriate for each service's call patterns, and which streaming models are incompatible with gRPC-Web if a browser client will ever need to call these services? How do gRPC status codes map to HTTP status codes at the API gateway, and what does the error propagation model look like for a client that only understands HTTP? What is the observability strategy for binary-encoded protobuf messages that cannot be inspected by standard HTTP logging middleware without a decoding step? Each of these questions has an answer that is not derivable from "we use gRPC" or "our protos are in the services repository." Each answer determines the coordination cost of schema changes, the client compatibility surface of the service mesh, and the probability that a streaming model chosen for performance will block a future frontend team from calling the service without a proxy layer. The answers exist in the AI sessions. They are the operational commitments behind the transport choice. They are almost never written down.
Two ways gRPC transport decisions produce the wrong outcome
The proto schema evolution coordination failure
A twelve-person engineering team at a B2B SaaS company migrates their internal service communication to gRPC in their second year. At the time of the migration, they have four internal services communicating over REST/JSON. The motivation is clear and well-founded: a specific API call between the data processing service and the notification service is generating twelve megabytes of JSON per request in their highest-traffic workflow, and the serialization overhead is measurable in the p99 trace. The team evaluates Protocol Buffers as an alternative serialization format and decides to adopt gRPC as the full transport layer rather than just switching serialization, because gRPC's code generation eliminates the manual client maintenance they have been doing as the REST API evolves. The decision produces immediate results: the data processing to notification service call drops from twelve megabytes of JSON to eight hundred kilobytes of protobuf, and the serialization overhead disappears from the p99 trace. The AI session that produced this decision is thorough, correct, and represents a genuine engineering improvement.
Three years later, the team has grown to thirty-one engineers and has fourteen internal gRPC services. A domain modeling exercise produces a finding: a field named user_id in the shared UserContext message type, used in eleven of the fourteen services' method signatures, should be renamed to account_id to reflect that the identifier references an account entity rather than a user entity — a distinction that has become important as the product's permission model has evolved to distinguish between account-level permissions and user-level permissions within an account. The rename is semantically correct and important for the domain model clarity. The engineering team's initial assessment is that it will be straightforward: proto3's field numbering means they can add a new account_id field with a new field number and deprecate user_id with the proto option [deprecated = true] and a comment explaining the replacement. The wire format is unchanged because proto3 serialization uses field numbers as identifiers, not field names. This assessment is correct about the binary encoding. It is incomplete about the coordination required.
The implementation begins and the coordination surface becomes visible. The eleven services that consume UserContext are owned by five different teams, deployed independently on cadences ranging from twice daily to twice weekly, and share the proto definition via a generated library that is vendored into each service's repository separately. The rename requires each consuming service to update its generated library and change all references from user_id to account_id in the service's own code. To perform the rename safely — with a deprecation window that allows all consuming services to migrate before the old field is removed — the team needs to: identify which services read user_id and populate it versus which only pass it through; coordinate across five teams with different priorities and deployment cadences; maintain backward compatibility in the shared message type while both user_id and account_id are present; and determine what version of the generated library constitutes "migration complete." None of these coordination requirements are documents that exist. The original proto schema documentation, which exists in comments in the proto file itself, contains no information about the schema ownership model (who has authority to add fields? deprecate fields? set the timeline for removal?), the breaking versus non-breaking change criteria (is removing a field breaking if it has been marked deprecated for six months and all consuming teams have had notice?), the deprecation window policy (how long must a deprecated field remain present before removal is permitted?), or the migration coordination protocol (which team is responsible for tracking migration progress across consuming services and asserting that removal is safe?).
Six months after the rename decision is made, three of the eleven services have migrated, two are partially migrated — populating both user_id and account_id as a compatibility measure — and six have not started because their teams have competing sprint priorities. The UserContext proto definition now carries both fields with the same content, doubling the field surface in every serialized message, and there is no agreed policy for when the removal of user_id is permitted. The team that initiated the rename has no authority to force the migration timelines of the other four teams and no escalation path that does not go through multiple engineering directors. The incoming technical leader who reviews this situation will find a shared proto definition with twelve deprecated fields accumulated across three years of similar rename and restructuring decisions, three partially-migrated services producing protobuf messages that populate both deprecated and replacement fields simultaneously, and no document explaining who owns the shared schema, what the deprecation window policy is, or what completion looks like for an in-progress migration. They will find the original gRPC migration decision clearly documented — "migrate to gRPC for the serialization performance benefits" — and no document explaining the schema evolution commitments that the migration to a shared binary contract between multiple teams required.
The browser client compatibility ceiling
A developer tools startup builds their internal service mesh using gRPC from the beginning. The founding engineering team has strong opinions about type safety and performance, and the AI session that designs the initial service architecture produces a clear rationale: code generation from proto definitions eliminates API client maintenance overhead, the binary wire format is efficient for high-volume inter-service calls, and server-side streaming is the correct model for the notification delivery service that needs to push updates to subscribers as they arrive without requiring the consumer to implement a polling loop. The team also implements bidirectional streaming for the session synchronization service, which needs to exchange state updates in both directions between the sync coordinator and the individual session workers. Both streaming choices are architecturally correct for the use cases and the available clients — all of which are server-side services that can use the full gRPC protocol over HTTP/2 with no restrictions. The founding session is technically sound. The proto schemas are clean. The services work correctly from the first deployment.
Two years later, a product decision is made to build a browser-based admin dashboard. The dashboard needs to display real-time notifications as they are delivered — subscribing to the notification delivery service's stream — and show live session synchronization state — connecting to the session synchronization service's bidirectional stream. The frontend engineering team estimates two weeks to build the dashboard UI once the gRPC service connections are established. The connection establishment takes longer than two weeks to even scope.
The incompatibility has two layers. The first layer is the protocol translation requirement. Browsers cannot initiate native gRPC calls because gRPC uses HTTP/2 in a way that browser APIs — the Fetch API and XMLHttpRequest — cannot access natively. Browsers use HTTP/2 for standard HTTPS connections, but do not expose the frame-level control required for gRPC's binary framing and trailer-based status code propagation. The gRPC-Web protocol solves this for most call types: it is a modified encoding that allows gRPC calls from browsers via a transcoding proxy, which receives gRPC-Web requests and translates them to standard gRPC before forwarding to the backend service. The team investigates gRPC-Web and finds that their notification delivery service's server-side streaming model is compatible with gRPC-Web, with some proxy configuration requirements for long-lived connection management. This is solvable in days.
The second layer is the bidirectional streaming limitation. The session synchronization service uses bidirectional streaming — the client sends a stream of state update messages and the server sends a stream of coordination messages, with both streams active simultaneously. Under gRPC-Web, client-side streaming and bidirectional streaming have no supported implementation in any stable proxy or server adapter. The grpc-web specification as implemented by Envoy, nginx-grpc-web, and the official grpc-web client library does not support sending a stream of request messages from a browser client; the protocol's design is built on the assumption that browsers can receive streaming responses (via XHR or fetch with streaming body) but cannot send streaming requests. Bidirectional streaming requires both directions to stream, and the client-send direction is architecturally unsupported in stable gRPC-Web implementations. The session synchronization service's streaming model has no supported path to direct browser access under any currently-stable gRPC-Web configuration.
The architectural response requires choosing between two options, both requiring engineering work not in the roadmap. Option one: build a server-side aggregation layer — a new WebSocket service that subscribes to the bidirectional gRPC stream on behalf of the browser client, exposes a WebSocket connection to the browser, and translates between the browser's WebSocket message format and the gRPC service's protobuf messages. This is three to four weeks of engineering work, adds a new service to maintain, and means the admin dashboard's real-time session state flows through three hops — browser to WebSocket service, WebSocket service to gRPC service — rather than two. Option two: refactor the session synchronization service's streaming model to unary calls or server-side streaming that is compatible with gRPC-Web, which requires changing the service's protocol and updating all existing server-side clients. Both options exist because the founding session chose bidirectional streaming without documenting "browser clients cannot call this service without a server-side aggregation layer, and bidirectional streaming is not supported in stable gRPC-Web implementations." The constraint was not visible at founding because there were no browser clients. Two years later, the constraint produces a roadmap disruption whose cost — three to four weeks of unplanned engineering work — exceeds what a compatibility matrix entry at design time would have cost.
Three structural properties that gRPC transport decisions determine
The schema ownership model and the coordination surface
A gRPC service's proto schema is a contract between the service and all of its clients. The contract's evolution — adding fields, deprecating fields, removing fields, changing field types, adding methods, removing methods — requires coordination between the service team and all client teams. The cost of that coordination is determined by the schema ownership model: who has the authority to change the proto definition, what process is required before a change is made, and who is responsible for ensuring that all clients have migrated before a breaking change is applied. Without a documented ownership model, these responsibilities default to whoever controls the proto repository, who must also determine on an ad hoc basis which clients exist, which are affected by each proposed change, and how to communicate migration requirements to teams with different deployment cadences and competing priorities.
The schema ownership model is the prerequisite for a deprecation policy. A deprecation policy that specifies "fields should be deprecated for six months before removal" requires that someone be responsible for tracking when deprecations were announced, monitoring which consuming services have migrated, and determining when the six-month window is satisfied. Without a named owner, the policy is a stated intention rather than an operational commitment, and the default accumulation behavior is the outcome of the UserContext rename: deprecated fields accumulate indefinitely because no team is unambiguously responsible for the removal, and the migration stalls at the point where the team with the highest migration cost and the lowest internal priority has not yet updated their service.
The coordination surface grows non-linearly with the number of consuming services and the number of teams that own them. At two consuming services owned by the same team, schema evolution requires internal coordination that the owning team controls. At four consuming services owned by two teams, cross-team coordination is manageable through direct communication. At eleven consuming services owned by five teams, the coordination surface requires a formal protocol: a change proposal process, an impact assessment step, a migration tracking mechanism, and an escalation path when a team's migration timeline conflicts with the removal window. The schema ownership decision at the time of the first shared proto definition determines which coordination model the team will need as the mesh grows. A team that defers the ownership decision to "we'll figure it out when we need it" will need it for the first cross-team schema change, which arrives not at two services but at the moment the first service is consumed by a team that does not own the proto definition.
The correct ownership model for a shared proto schema depends on the organization's structure and service topology. In a single-team organization that owns all services, the owning team is the schema owner and can make changes with only internal coordination. In a multi-team structure where a shared proto is consumed across team boundaries, the schema must have an explicitly designated owning team — typically the team whose service is the authoritative producer of the data the message type represents — with a documented change proposal protocol and a breaking change classification. The decisions never written down problem is most acute for shared proto schemas at the moment of cross-team sharing: the schema is designed in an internal context where the ownership is obvious (we wrote it, we own it), and the documentation that is missing is the documentation that would be needed when the schema is first shared with a team that cannot make the same inference. By the time the first cross-team consumer exists, the schema has been in production for months or years, and the original AI session that defined it is not in any document a new consumer can read.
The streaming model and the client compatibility ceiling
The gRPC protocol supports four call types: unary (one request, one response), server-side streaming (one request, a stream of responses), client-side streaming (a stream of requests, one response), and bidirectional streaming (a stream of requests and a stream of responses). Each call type is suited to different use cases and is the correct model for specific service communication patterns. Unary calls are appropriate for standard request-response patterns where the response is complete and bounded in size. Server-side streaming is appropriate for result sets that exceed a single response, for real-time push where the server needs to send updates as they arrive, or for long-running operations that report progress incrementally. Client-side streaming is appropriate for bulk data ingestion where the client sends a sequence of records and receives a completion acknowledgment. Bidirectional streaming is appropriate for interactive protocols where both sides produce data in response to the other, such as session coordination, distributed locking with heartbeats, or chat. The streaming model choice is an architectural decision at the service design layer — but its client compatibility consequences are often not visible until a new category of client needs to call the service.
The browser compatibility constraint is the streaming model decision most likely to surface as a future architectural blocker. Standard gRPC uses HTTP/2 with binary framing and trailer headers — a combination that browser JavaScript cannot access natively through the Fetch API or XMLHttpRequest. The gRPC-Web protocol addresses this for most call types, with compatibility profiles that vary by streaming model. Unary calls are fully supported under gRPC-Web across all implementations and client libraries. Server-side streaming is supported in most gRPC-Web implementations via long-lived XHR or fetch connections, with proxy and load balancer configuration requirements for connection timeout management that differ from short-lived requests. Client-side streaming is not supported in gRPC-Web because browsers cannot control HTTP/2 stream framing in a way that allows sending a sequence of binary-framed request messages — the browser can receive a streaming response but cannot produce a streaming request. Bidirectional streaming is not supported in any stable gRPC-Web proxy or server adapter implementation because it requires both streaming directions simultaneously, and the client-send direction is architecturally unavailable in browser environments under the gRPC-Web protocol as currently specified and implemented.
These compatibility profiles create a client compatibility ceiling that is determined at service design time. A service that uses unary calls can be called from any client that implements gRPC or gRPC-Web, including future browser clients, without architectural changes to the service. A service that uses server-side streaming can be called from browser clients through a gRPC-Web proxy with specific configuration, but the long-lived connection requirements add operational complexity. A service that uses client-side streaming or bidirectional streaming cannot be called from browser clients without a server-side aggregation layer that translates between the browser's accessible protocols (WebSocket, SSE, standard HTTP) and the gRPC service's streaming model. The aggregation layer is a permanent piece of infrastructure — it is not a temporary workaround that disappears when gRPC-Web matures — because the architectural limitation is in the browser's HTTP API surface, not in the gRPC-Web specification's maturity.
The streaming model policy must be documented at service design time, before the client compatibility ceiling is encountered. The policy should specify which streaming models are permitted for services that are internal-only and will never be called from browser or HTTP clients; which streaming models require explicit documentation of client compatibility constraints if used for services that may be called from browser or HTTP clients; and what the architectural response is when a browser or HTTP client needs to call a service with a streaming model that gRPC-Web does not support. The documentation of this policy at design time does not prevent the team from choosing bidirectional streaming when it is the correct model for the service's communication pattern — it ensures that the choice is made with awareness of the client compatibility constraint rather than producing a roadmap disruption two years later when the first browser client needs to call the service. The API gateway decision record must be coupled to the streaming model policy: the gateway is the component that translates between browser clients and gRPC services, and the gateway's capabilities constrain which streaming models can be exposed to browser clients without a server-side aggregation layer.
The error model and the observability surface
gRPC defines a standard set of status codes with defined semantics: OK for successful completion, INVALID_ARGUMENT for malformed or invalid client-specified parameters, UNAUTHENTICATED for missing or invalid credentials, PERMISSION_DENIED for valid credentials with insufficient permission, NOT_FOUND for requested resources that do not exist, ALREADY_EXISTS for creation of a resource that is already present, RESOURCE_EXHAUSTED for rate limit violations or quota exhaustion, INTERNAL for unexpected server-side conditions, UNAVAILABLE for transient service unavailability that the client should retry, and UNKNOWN for errors that cannot be classified. The status code model is more semantically specific than HTTP status codes for server-side error categorization, but it requires a mapping decision at every point where gRPC services are exposed to clients that use HTTP — an API gateway, a gRPC-Web proxy, or a REST transcoding layer.
The error model has two failure modes in practice. The first is status code inconsistency across services. When different services in the same mesh use gRPC status codes inconsistently — one service returns FAILED_PRECONDITION for a domain validation error and another returns INVALID_ARGUMENT for the same class of error — the error model becomes unpredictable to client code that handles errors programmatically. A client interceptor that catches INVALID_ARGUMENT to display a user-friendly validation message will fail silently for services that return FAILED_PRECONDITION for the same validation failure, because FAILED_PRECONDITION means "the operation was rejected because the system is not in a state to execute it" — semantically, a conflict error — not "the request parameters are invalid." The inconsistency is discovered at integration time when the client handles one service's errors correctly and another's incorrectly, typically under the specific error condition rather than during happy-path development and testing.
The second failure mode is gateway translation loss. The standard mapping between gRPC status codes and HTTP status codes maps INVALID_ARGUMENT to HTTP 400, UNAUTHENTICATED to HTTP 401, PERMISSION_DENIED to HTTP 403, NOT_FOUND to HTTP 404, RESOURCE_EXHAUSTED to HTTP 429, and UNAVAILABLE to HTTP 503. Critically, UNKNOWN, INTERNAL, FAILED_PRECONDITION, ABORTED, OUT_OF_RANGE, and DATA_LOSS all map to HTTP 500. An application error that is semantically a validation error — which should produce HTTP 400 and inform the client not to retry — but is returned as UNKNOWN or INTERNAL because the service team treated these codes as "something went wrong" categories rather than specific semantic signals, maps to HTTP 500 at the gateway. External clients that receive HTTP 500 may retry the request with exponential backoff, consuming the service's rate limit quota and contributing to load during periods where validation errors are high-frequency. The error model decision determines whether external clients receive semantically accurate HTTP status codes that allow them to distinguish retriable server errors from non-retriable client errors, or receive HTTP 500 for a significant fraction of errors because the service's internal status code vocabulary is not aligned with the gateway's translation mapping.
The observability surface for gRPC differs from REST/JSON in one critical dimension: protobuf message bodies are binary-encoded and cannot be logged or inspected by standard HTTP logging middleware without a decoding step. A standard access log captures the gRPC method name (the path in the form /package.ServiceName/MethodName), the HTTP status, and the request duration — sufficient for traffic monitoring and latency alerting. What the access log cannot capture is the content of the request or response body, which is binary. Debugging a specific production error that requires understanding the request's field values — reproducing a data corruption, correlating a specific customer's request with a downstream side effect, or understanding why a particular request returned an unexpected status code — requires either proto schema registration in a logging layer that can decode the binary bodies, gRPC server interceptors that log decoded fields before serialization and after deserialization, or a service mesh that integrates with a proto schema registry to decode traffic at the proxy level. The absence of this logging infrastructure does not affect operational monitoring — per-method error rates and latency are captured correctly from access log metadata — but it produces a debugging gap that is discovered during the first production incident that requires understanding request field values rather than just status codes and latency. The observability strategy decision record must address binary-encoded traffic explicitly if the service mesh uses gRPC, because the standard observability stack built for HTTP/JSON traffic is incomplete for a gRPC service mesh without proto schema integration.
Three AI session types that embed gRPC transport decisions without documenting them
The inter-service communication session is where gRPC is chosen and the first proto schema is defined. The session evaluates gRPC's performance characteristics against REST/JSON for the specific service pair at hand, produces a clear decision with correct performance reasoning, and defines the proto file for the first service interface. The session produces working code: a proto definition, generated stubs, a server implementation, and a client call. What the session does not produce is the governance model for the schema: who has authority to change the proto definition now that it exists? What is the review requirement before a field is added or deprecated? What is the breaking change classification that distinguishes safe changes from changes that require coordinated client migration? These questions are not part of "define the gRPC service interface" in the way teams frame the task — they are the operational commitments that determine whether the proto schema remains a clean contract or accumulates deprecated fields at the rate of schema evolution decisions made without a deprecation window policy. The schema ownership model becomes necessary the moment the second team adds a service that consumes the proto definition; the deprecation window policy becomes necessary the moment the first field needs to change; the breaking change classification becomes necessary the moment the first field removal is proposed. Each of these moments arrives separately, months or years after the founding session, when the context of the original decision is partially or fully lost and the only documentation is the proto file itself. The open-source extractor surfaces these founding inter-service communication sessions from AI chat history, recovering the schema design rationale and the original streaming model decisions — context that makes visible which operational commitments were implicit in the original session and which were never addressed.
The performance migration session produces the most consequential gRPC decisions for long-term observability. The session addresses a measured performance problem — a specific service-to-service call with measurable serialization overhead — and produces a technically correct solution: migrate from JSON to protobuf, adopt gRPC as the full transport layer, and benchmark the improvement. The session is focused on the performance outcome, which is immediate and verifiable. What the session does not produce is the observability plan for the migrated service: how does the team debug specific request failures in the new binary-encoded service? What server interceptors are required to maintain the same level of per-request logging that the REST service provided? If the migrated service is exposed through an API gateway, how does the gRPC error model translate to HTTP status codes for external clients, and does the current error handling in the service produce the correct gRPC status codes for each error category? These observability and error model questions are downstream of the migration decision but are not part of the migration task as it is typically framed — "switch from REST to gRPC" — because the immediate deliverable is a working gRPC service, and the observability gaps in a working gRPC service are not visible until the first incident that requires debugging a specific request's field values or until the first report from an external client that HTTP 500s are being received for validation errors. The data serialization decision record is closely related: the choice of protobuf as the serialization format is part of the performance migration decision, and the proto schema's design — field numbering, message type hierarchy, use of well-known types — determines the schema evolution surface that the ownership model and deprecation policy will govern.
The API gateway integration session is where the gRPC mesh first becomes visible to clients outside the service mesh. The session selects the gateway technology, configures the gRPC transcoding or gRPC-Web proxy layer, and produces a working configuration that allows HTTP clients to call gRPC services through the gateway. The session may produce the HTTP-to-gRPC method mapping and a basic gRPC status to HTTP status mapping. What the session does not produce is the error detail propagation policy: gRPC supports rich error details via the google.rpc.Status message with typed error detail payloads that carry machine-readable structured information about what went wrong — which field failed validation, what the conflict was, what the retry-after delay is. Does the gateway pass these detail payloads to HTTP clients, and if so, in what format — the raw grpc-status-details-bin trailer, a JSON-serialized body, or a gateway-specific error envelope? The error detail propagation policy determines whether external clients receive actionable error context or receive only the HTTP status code and a generic error message. The session also does not produce the mTLS policy between the gateway and the internal gRPC services. A gRPC service mesh where service-to-service traffic uses mTLS but the gateway-to-service connection relies on the internal network's security rather than transport-layer encryption creates a compliance gap for organizations whose security requirements mandate encryption in transit across all internal service boundaries — a requirement that the API gateway integration session is unlikely to surface unless security requirements are explicitly part of the session prompt. The service mesh decision record and the API gateway decision record are coupled through the mTLS policy: the service mesh's mTLS configuration determines the encryption model between services, and the gateway's connection to the mesh must be explicitly included in or excluded from that model.
The five sections of a gRPC decision record
The first section documents the transport protocol selection and the performance versus compatibility trade-off rationale. The protocol selection rationale must be specific and evaluatable: "gRPC was selected for the data processing to notification service call because that call was generating twelve megabytes of JSON per request at peak throughput, and the serialization overhead was contributing eight milliseconds to the p99 trace; protobuf serialization reduces the payload to eight hundred kilobytes for the same data structure and the generated stubs eliminate the manual client maintenance that JSON schema evolution has been requiring" is evaluatable against "has JSON serialization overhead appeared in a p99 analysis for any new service since this decision was made, and is the overhead comparable to the threshold that justified the original migration?" — the trigger for reviewing whether gRPC should be extended to additional services. The section must also document the boundary between services that use gRPC and services that use REST or other protocols, and the policy for a service that starts as an internal gRPC service and later needs to become externally accessible. Without a documented protocol boundary, individual teams make protocol decisions independently, producing a mesh where some internal services communicate over REST and others over gRPC with no consistent rationale, and where a service that is later exposed externally must be evaluated for protocol translation requirements that were not anticipated at design time. The microservices decision record is the predecessor document: the decision to extract services is prior to the decision about how they communicate, and the service topology determined by the microservices decision constrains the ownership model for shared proto schemas.
The second section documents the proto schema ownership model, the evolution policy, and the breaking change criteria. The schema ownership section must name the owning team for each shared proto definition, specify the change proposal process for changes that affect consumers outside the owning team, and define the breaking versus non-breaking change classification. At minimum, the breaking change classification must address: field removal (breaking at the application level regardless of proto3's silent-default behavior at the wire level — remove only after all consumers have migrated and the old field reference has been removed from all consumer codebases); field type changes (breaking in almost all cases — a field type change that is safe at the binary level, such as widening an int32 to int64, may produce unexpected behavior in generated language bindings); method removal from a service definition (breaking for all clients that call the removed method — requires a deprecation window and client migration before removal); and new required-by-convention fields added to messages that existing consumers populate (breaking if the field is required for the service to function correctly, even though proto3 has no required field concept at the schema level). The deprecation window policy must specify the minimum time between deprecating a field and permitting its removal, the mechanism for tracking migration progress across consuming services, and the authority that certifies migration as complete. The section must also specify the proto schema registry: where the authoritative proto files are stored, what the review requirement is for PRs that change proto definitions consumed by services outside the owning team, and how version management works for the generated library that consuming services vendor.
The third section documents the streaming model selection for each service and the client compatibility matrix. For each service in the mesh, the streaming model section records: the streaming model used (unary, server-side streaming, client-side streaming, or bidirectional streaming), the rationale for that model given the service's communication pattern, and the client compatibility profile under that model. The compatibility profile specifies: which client types can call the service directly with no proxy layer (gRPC-native clients in supported languages); which client types can call the service via a gRPC-Web proxy (browser clients for unary and server-side streaming, with specific proxy configuration requirements documented); and which client types cannot call the service without a server-side aggregation layer that translates between the client's accessible protocol and the service's streaming model (browser clients for client-side streaming and bidirectional streaming). The compatibility matrix is the mechanism that prevents the browser client compatibility ceiling discovery story: a dashboard team that can consult the compatibility matrix before project specification discovers the aggregation layer requirement at planning time rather than after two weeks of discovery. The section must also document the policy for future streaming model changes: whether a service can migrate from unary to server-side streaming, what the coordination requirement is, and what the impact is on existing clients. Streaming model changes are breaking changes for any client that expects a unary response and must handle a streaming response after the change, even if the service's proto definition remains valid.
The fourth section documents the error model design and the gateway error translation policy. The error model section must specify the standard gRPC status code for each category of application error the service can produce. At minimum, the specification must distinguish validation errors from authorization errors from not-found errors from conflict errors from rate limiting from internal errors, and must assign a specific gRPC status code to each category with the rationale for the assignment. The specification must address the UNKNOWN code explicitly: UNKNOWN is the correct code for errors that genuinely cannot be classified, but it maps to HTTP 500 at the gateway and triggers client retry behavior appropriate for a server-side error; using UNKNOWN as a default error code for all unexpected conditions inflates the HTTP 500 rate for errors that should be HTTP 400 or HTTP 409. The section must also document the error detail propagation model: when the status code alone is insufficient — a validation error on a specific field in a multi-field request requires identifying which field failed — the service must use google.rpc.Status.details to attach typed error detail messages, and the gateway configuration must specify how those detail payloads are serialized in the HTTP response body for external clients. The gateway translation policy is the operational expression of the error model: it specifies how each gRPC status code maps to an HTTP status code at the gateway, how the grpc-message trailer is translated to an HTTP error response body, and whether the gateway passes structured error detail payloads or only the status code and message string to HTTP clients. The API versioning decision record is coupled to the error model: when the service's error taxonomy changes — adding new error categories, changing the status code assignment for an existing category — existing clients may have error handling logic that assumes the old taxonomy, and the versioning policy must address how error model changes are communicated and versioned alongside the proto schema.
The fifth section documents the observability strategy for binary-encoded traffic and the proto schema registry. The observability section must specify three components. First, per-method server-side metrics: the gRPC server framework's interceptor or middleware hooks must be configured to emit per-method request count, error rate by gRPC status code, and request latency at P50/P95/P99 percentiles, exported to the team's metrics system. The metric label model must include at minimum the service name, the method name, and the gRPC status code, so that error rate dashboards can distinguish between INVALID_ARGUMENT errors (client validation failures that should not trigger on-call alerts) and INTERNAL errors (server-side failures that should). Second, structured request logging: the logging interceptor must capture the method name, request duration, gRPC status code, and a request identifier for correlation with downstream service calls, for every request. The policy for logging request and response bodies must address privacy and performance: full body logging is expensive for high-throughput services and may include fields that should not appear in logs; the policy should specify which fields, if any, are extracted and logged from request bodies, and what the retention period is for logs that contain decoded field values. Third, proto schema registry and debugging workflow: the section must specify where the authoritative proto definitions are stored and how the version of a proto definition can be identified from a message captured in production traffic — specifically, which proto schema version was in use when a specific deployment was active. This information is required when debugging a production error that requires decoding a binary message captured in a trace or a network capture: without the schema version mapping, decoding requires guessing which proto definition applies, which is unreliable when the proto schema has evolved. The debugging workflow for a binary-encoded gRPC service requires access to the proto schema registry; without it, the debugging workflow for binary-body errors is fundamentally more complex than for JSON-body REST services, and this complexity should be accounted for in the decision to adopt gRPC rather than discovered during the first production incident that requires body-level debugging.
The inter-service communication session, the performance migration session, and the API gateway integration session each produce gRPC transport decisions whose long-term coordination cost — a six-month schema migration stall caused by eleven services owned by five teams with no agreed schema ownership model, deprecation window policy, or migration tracking mechanism for a field rename that is correct at the binary level and a multi-team coordination problem at the code level, or two to four weeks of unplanned engineering work caused by a bidirectional streaming model that is architecturally correct for server-side clients and has no supported path to browser access in any stable gRPC-Web implementation — exceeds what a schema ownership document and a streaming compatibility matrix would have cost at the time the decision was made. The decisions are in the AI chat history: the performance analysis that motivated the gRPC adoption, the streaming model that was chosen for the notification delivery service, the gateway configuration that translated gRPC status codes to HTTP status codes for external clients. WhyChose's open-source extractor surfaces these founding gRPC sessions as structured decision records before the next schema evolution coordination failure, the next browser client compatibility discovery, or the next gateway error translation gap makes the undocumented operational commitments visible as a multi-team coordination problem or a frontend architectural blocker. The new CTO onboarding problem in the context of a gRPC service mesh is a schema ownership problem: an incoming technical leader will find a proto schema repository with twelve deprecated fields across six message types, several partially-migrated consuming services populating both deprecated and replacement fields, and no document explaining who owns the shared schemas, what the deprecation window policy is, or which consuming services have completed which migration.
Further reading
- Decisions never written down — gRPC schema ownership and streaming model decisions as founding choices whose coordination consequences compound silently until a field rename requires six months of cross-team migration coordination or a browser client requirement surfaces a streaming model incompatibility two years after the founding session
- The new CTO onboarding problem — an incoming technical leader finds a gRPC service mesh with deprecated fields across shared proto definitions, partially-migrated consuming services, no schema ownership document, and no explanation of which streaming models can or cannot be called from browser clients
- The API gateway decision record — the API gateway is the component that translates gRPC status codes to HTTP status codes for external clients and implements the gRPC-Web proxy layer for browser clients; the gateway's capabilities constrain which streaming models can be exposed to browser clients and how error details are serialized in HTTP responses
- The service mesh decision record — the service mesh determines the mTLS policy between internal gRPC services and the gateway, the traffic inspection capabilities for binary-encoded protobuf traffic, and the proxy configuration for gRPC-Web streaming connections
- The microservices vs. monolith decision record — the decision to extract services is the predecessor to the inter-service communication protocol decision; the service topology determines the shared proto schema surface and the number of teams that must coordinate on schema evolution
- The API versioning decision record — the proto schema deprecation window policy and the breaking change classification are the versioning policy for gRPC service contracts; the versioning strategy must address how error model changes, field removals, and streaming model changes are versioned and communicated to consuming services
- The data serialization decision record — the choice of protobuf as the serialization format is part of the gRPC adoption decision; the proto schema's field numbering model, use of well-known types, and message type hierarchy determine the schema evolution surface that the ownership model and deprecation policy govern
- WhyChose extractor — surfaces the founding inter-service communication session, the performance migration session, and the API gateway integration session from AI chat history as structured decision records, recovering the streaming model rationale, the original schema design decisions, and the error model commitments that were implicit in the founding team's shared context