A deep dive into gRPC and REST across five architectural pillars: serialization efficiency, HTTP/2 multiplexing, contract-first governance, streaming patterns, and mTLS security. Contract-first gRPC server and client code across seven backend stacks, plus a decision matrix for Open Banking versus internal microservices.
Emmanuel Maneswa
Full Stack Software Engineer
Every bank now runs two API surfaces that look nothing alike. On the outside: Open Banking under PSD2 and the UK's Open Banking Standard, public third-party integrations, and mobile clients, all expecting REST over JSON because that's what the entire web understands. On the inside: core banking, fraud detection, and settlement engines calling each other thousands of times per second, where the cost of a verbose, un-typed, text-parsed payload compounds with every hop.
REST and gRPC are not competitors fighting for the same job. They're two protocols built for two different trust boundaries, and the architectural mistake I see most often is picking one and forcing it across both. This post is a working reference for when each one is the right call, backed by the network physics that actually drive the decision, not just protocol preference.
Open Banking regulation created a genuine requirement for REST: PSD2 in the EU and the UK's Open Banking Standard both mandate JSON-over-HTTPS APIs so that third-party providers, budgeting apps, payment initiation services, account aggregators, can integrate without needing anything beyond a standard HTTP client. This is a deliberate, correct choice: the API's consumers are external, numerous, and unknown at design time, and REST's universal tooling and human-readable payloads lower the integration bar for everyone.
Internally, the picture is different. A single customer-facing balance check might fan out into calls to the core banking ledger, a fraud scoring service, and a risk engine, all before the response reaches the client. Each of these is an internal microservice with a known, versioned contract and a latency budget measured in single-digit milliseconds. This is where REST's overhead starts to matter in a way it never does for a one-off public API call.
The real cost is network physics, not developer preference. JSON is a text format: every field name is repeated in every message, every number is parsed as a string and converted, and every HTTP/1.1 request typically opens (or reuses from a limited pool) its own TCP connection. At the volume a settlement engine or a fraud pipeline runs at, thousands of internal calls per second, the CPU cycles spent on JSON serialization and parsing, and the connection overhead of HTTP/1.1, become a measurable fraction of total latency. gRPC, built on HTTP/2 and Protocol Buffers, was designed specifically for this: binary serialization, a single multiplexed connection per service, and a schema contract enforced at compile time rather than hoped for at runtime.
The diagram makes the physical difference concrete. Under REST/HTTP1.1, each downstream call, balance, then risk score, opens its own connection and completes serially from the gateway's perspective. Under gRPC/HTTP2, both calls travel as independent streams over the same multiplexed TCP connection and can be issued concurrently. The gateway isn't doing anything architecturally different in either case, the transport protocol itself is what changes the shape of the request timeline.
Protocol Buffers encode messages as binary, with fields identified by number rather than repeated string keys, and typed at the schema level (int64, a structured decimal, a nested message) rather than left to runtime string parsing. JSON, by contrast, repeats every field name in every message and represents every number as a string that must be parsed and validated on receipt.
| Metric | JSON (REST) | Protocol Buffers (gRPC) |
|---|---|---|
| Encoding | UTF-8 text, human-readable | Binary, field-tagged |
| Field identification | Repeated string keys per message | Numeric field tags, defined once in the schema |
| Typical payload size (a balance response) | Larger — field names repeated, numbers as strings | Commonly 60–80% smaller for the same data |
| Serialization/deserialization cost | Higher — string parsing and type coercion on every field | Lower — binary decode against a known schema, no runtime type guessing |
At low request volumes, this difference is invisible. At the volume a real-time settlement or fraud pipeline runs at, it is a measurable, compounding cost, both in server CPU spent on serialization and in bytes moved across the network.
HTTP/1.1 has head-of-line blocking on a per-connection basis: a slow response blocks everything queued behind it on that connection, which is why HTTP/1.1 clients open connection pools to compensate. HTTP/2, which gRPC is built on, multiplexes many independent streams over a single TCP connection, so a slow response on one stream never blocks another. gRPC also supports server push and native streaming (covered below), none of which REST/HTTP1.1 offers without workarounds like long-polling or Server-Sent Events.
REST APIs are typically documented after the fact, or alongside development, with OpenAPI/Swagger specs that describe the contract but don't enforce it, nothing stops a service from returning a field the spec doesn't mention, or omitting one it promises. gRPC inverts this: the .proto file is the contract, and both client and server stubs are generated from it. A field renamed or removed in the .proto is a compile error in every consuming service, not a runtime surprise discovered when a payment fails to parse in production. In a multi-team banking integration estate, this is the difference between schema drift being caught in code review and schema drift being caught by an incident.
gRPC defines four communication patterns; REST, built on request-response, has none of them natively.
| Pattern | Description | Banking Example |
|---|---|---|
| Unary | One request, one response — the REST-equivalent shape | GetBalance(accountId) → Balance |
| Server Streaming | One request, a stream of responses | Real-time account balance or market-data feed pushed to a client as it changes |
| Client Streaming | A stream of requests, one response | Bulk batch payment upload, where the client streams thousands of payment records and receives a single summary result |
| Bidirectional Streaming | Both sides stream independently over the same call | Real-time order-book matching, where price updates and order submissions flow concurrently in both directions |
Server-streaming and bidirectional streaming in particular have no clean REST equivalent; approximating them requires polling, Server-Sent Events, or WebSockets bolted on beside the REST API rather than a native part of the protocol.
Both protocols support TLS, but gRPC's ecosystem treats mutual TLS (mTLS) as a first-class deployment pattern for service-to-service authentication, each side presents and verifies a certificate, not just the server. Authentication tokens (OAuth2 bearer tokens) and tracing data (correlation IDs) travel as gRPC metadata, attached via client and server interceptors that run on every call without the application code needing to remember to add a header each time. REST achieves the same outcome with HTTP headers and middleware, functionally similar, but gRPC's interceptor model makes it structurally harder to forget, since interceptors are configured once at the channel level rather than per-request.
This is the pattern that resolves the REST-vs-gRPC debate in practice: don't choose one, segment by trust boundary. REST serves the edge, mobile apps and the Open Banking portal, where universal HTTP compatibility matters more than raw throughput. An Envoy gateway (or an equivalent edge proxy) translates that inbound REST/JSON traffic into gRPC/ProtoBuf calls against internal services. Everything behind the gateway, core banking, the risk engine, the ledger, speaks gRPC to everything else, including service-to-service calls that never touch the edge at all.
REST vs gRPC: Comprehensive Comparison
| Metric / Feature | REST (JSON / HTTP/1.1) | gRPC (ProtoBuf / HTTP/2) | Architectural Tradeoff / Verdict |
|---|---|---|---|
| Serialization Speed | Text parsing, slower at scale | Binary encoding, 3–10x faster | gRPC wins for high-frequency internal calls |
| Payload Size | Verbose, field names repeated | Compact, field numbers not names | gRPC typically 60–80% smaller on the wire |
| Schema Enforcement | Optional (OpenAPI, often drifts) | Mandatory (.proto contract) | gRPC prevents schema drift by construction |
| Browser Compatibility | Native, universal | Requires a grpc-web proxy | REST wins for direct browser clients |
| Streaming Capabilities | Polling or SSE workarounds | Native unary/server/client/bidi streaming | gRPC wins for real-time feeds |
| Latency / Throughput | Higher — text parsing, per-call connections | Lower — multiplexed HTTP/2, binary frames | gRPC wins under high concurrency |
| Learning Curve | Low — ubiquitous tooling | Higher — protoc toolchain, codegen | REST wins for onboarding speed |
| Ideal Banking Use Case | Open Banking, public APIs, web clients | Core banking microservices, settlement, streaming | Use both, segmented by trust boundary |
Read this table the same way as the pattern comparison in a companion post on this site: it's a decision aid tied to trust boundaries, not a universal ranking. Every row where gRPC wins is a row where the caller and callee are both under your control and can share a generated stub. Every row where REST wins is a row where the caller is external, browser-based, or unknown at design time.
API Gateways & Edge Proxies. Envoy is the de facto standard for REST-to-gRPC translation at the edge, with native HTTP/2 and gRPC-Web support. Kong and Apigee serve the same edge-gateway role with a stronger focus on API management, rate limiting, and developer portals for external Open Banking consumers. Where a browser client needs to call a gRPC service directly (rather than through a REST-translating gateway), a gRPC-Web proxy is required, since browsers cannot originate raw HTTP/2 trailers-based gRPC calls natively.
Middleware Integration Platforms. Intellect iTurmeric and Olive Fabric, already covered in this site's core banking integration post, provide banking-specific adapters that can sit in front of either protocol. Spring Cloud Gateway is the equivalent general-purpose choice in Java/Spring estates, commonly used to front internal gRPC services with a REST-compatible edge.
Testing & Debugging Tooling. Postman and Swagger UI remain the default tools for exploring and testing REST APIs interactively. grpcurl is the command-line equivalent for gRPC, a curl-like tool that can call a gRPC service using its reflection API without needing generated client code. BloomRPC (and its actively maintained successors) provides the GUI equivalent, a Postman-style interface for exploring and invoking .proto-defined services.
account_service.protoEvery gRPC integration starts with the .proto file. Note the use of google.type.Decimal for monetary amounts, never a native float or double, which cannot represent currency values exactly and will silently accumulate rounding error across enough transactions.
syntax = "proto3";
package banking.account.v1;
import "google/protobuf/timestamp.proto";
import "google/type/decimal.proto";
option csharp_namespace = "Banking.Account.V1";
option java_package = "com.bank.account.v1";
option go_package = "github.com/bank/account/v1;accountv1";
// AccountService exposes balance and transaction operations for core banking accounts.
service AccountService {
// GetBalance returns the current balance for a single account (unary RPC).
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);
// StreamTransactions streams transaction history for an account as it is written (server-streaming RPC).
rpc StreamTransactions(StreamTransactionsRequest) returns (stream Transaction);
}
message GetBalanceRequest {
string account_id = 1;
string correlation_id = 2;
}
message GetBalanceResponse {
string account_id = 1;
google.type.Decimal available_balance = 2; // structured decimal — never a float/double for money
google.type.Decimal ledger_balance = 3;
string currency_code = 4; // ISO 4217, e.g. "USD"
google.protobuf.Timestamp as_of = 5;
}
message StreamTransactionsRequest {
string account_id = 1;
google.protobuf.Timestamp since = 2;
}
message Transaction {
string transaction_id = 1;
string account_id = 2;
google.type.Decimal amount = 3;
string currency_code = 4;
string description = 5;
google.protobuf.Timestamp posted_at = 6;
}
Every field carries an explicit number, never reused across schema versions, which is how ProtoBuf achieves forward and backward compatibility: new fields get new numbers, old consumers simply ignore fields they don't recognize, and nothing shifts underneath a deployed client.
The discipline that matters most on the server side: never let an unhandled exception reach the client as an opaque UNKNOWN status. Validate input explicitly, and map every domain failure to the gRPC StatusCode that actually describes it, InvalidArgument for bad input, NotFound for a missing account, so the client can make a correct retry decision instead of guessing.
gRPC Server: Unmapped Exceptions vs Proper Status Codes
1// C#: gRPC server — validated input, exceptions mapped to gRPC status codes
2public override async Task<GetBalanceResponse> GetBalance(GetBalanceRequest request, ServerCallContext context)
3{
4 if (string.IsNullOrWhiteSpace(request.AccountId))
5 throw new RpcException(new Status(StatusCode.InvalidArgument, "account_id is required"));
6
7 var account = await _accountService.FindAsync(request.AccountId);
8 if (account is null)
9 throw new RpcException(new Status(StatusCode.NotFound, $"account {request.AccountId} not found"));
10
11 return new GetBalanceResponse
12 {
13 AccountId = account.Id,
14 AvailableBalance = account.AvailableBalance.ToProtoDecimal(), // structured decimal, no precision loss
15 CurrencyCode = account.CurrencyCode,
16 AsOf = Timestamp.FromDateTime(DateTime.UtcNow)
17 };
18}The discipline that matters most on the client side: every internal call carries mTLS, an OAuth2 bearer token, and a correlation ID, attached once via an interceptor rather than remembered per call site. A plaintext, unauthenticated, untraceable internal gRPC call is exactly as dangerous inside the bank's perimeter as an unauthenticated REST call would be at the edge.
gRPC Client: Plaintext No-Auth vs mTLS + OAuth2 + Correlation ID
1// C#: gRPC client — mTLS channel, OAuth2 token and correlation ID via ClientInterceptor
2var channel = GrpcChannel.ForAddress("https://core-banking:5000", new GrpcChannelOptions
3{
4 HttpHandler = new SocketsHttpHandler { SslOptions = new SslClientAuthenticationOptions
5 {
6 ClientCertificates = new X509CertificateCollection { _clientCertificate } // mTLS
7 }}
8});
9
10var invoker = channel.Intercept(new AuthAndCorrelationInterceptor(_tokenProvider));
11var client = new AccountService.AccountServiceClient(invoker);
12var response = await client.GetBalanceAsync(new GetBalanceRequest { AccountId = accountId });
13
14// AuthAndCorrelationInterceptor attaches Authorization + x-correlation-id to every call's Metadata
15public class AuthAndCorrelationInterceptor : Interceptor
16{
17 public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
18 TRequest request, ClientInterceptorContext<TRequest, TResponse> context,
19 AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
20 {
21 var headers = context.Options.Headers ?? new Metadata();
22 headers.Add("authorization", $"Bearer {_tokenProvider.GetToken()}");
23 headers.Add("x-correlation-id", Activity.Current?.Id ?? Guid.NewGuid().ToString());
24 var newOptions = context.Options.WithHeaders(headers);
25 return continuation(request, new ClientInterceptorContext<TRequest, TResponse>(
26 context.Method, context.Host, newOptions));
27 }
28}REST remains king when:
gRPC is mandatory when:
.proto contract and there's no external consumer to accommodate.REST and gRPC solve different problems, and the strongest banking API architectures use both, deliberately segmented by trust boundary rather than chosen once for the whole estate. REST's universal HTTP compatibility and human-readable payloads make it the correct, often regulator-mandated, choice at the public edge: Open Banking, third-party integrations, browser clients. gRPC's binary serialization, HTTP/2 multiplexing, contract-first schema enforcement, and native streaming make it the correct choice for the high-volume, low-latency internal microservice mesh behind that edge, core banking, risk, settlement, and fraud detection calling each other thousands of times a second.
The architectural mistake is treating this as a single either-or decision. In the banking integration systems I've worked on, the pattern that scales cleanly is REST at the edge, translated by a gateway like Envoy into gRPC across every internal service boundary, with the .proto contract acting as the single source of truth that keeps every internal team's schema honest. Choose the protocol the trust boundary and latency budget actually demand, not the one that happens to be familiar.