gRPC versus REST: picking protocols at a service boundary

2026-08-23 · Transcendence

The Transcendence Casino is a three-tier system on purpose: a Go and Gin backend facing the browser, a standalone C++ engine that owns the actual game rules (card shuffling, hand evaluation, poker betting rounds, slot RNG), and a React frontend on top. Two service boundaries, two very different sets of constraints — and using the same protocol for both would have been the wrong call in either direction.

The browser-facing edge: REST and WebSockets

Between the frontend and the Go backend, the requirements are: browsers have to be able to speak it natively, it has to be debuggable with curl or the network tab at 2am, and it has to support a long-lived connection for real-time table state. That's REST over HTTP for the stateless calls — auth, wallet balance, match history — and a hand-rolled WebSocket hub for everything that changes mid-hand: card deals, bets, chat, spectator updates. JSON's human-readability is a genuine feature here, not just a tax, because the people debugging this boundary are frontend engineers and me at odd hours, and every browser devtool already understands it for free.

The internal edge: why gRPC for Go ↔ C++

The Go-backend-to-C++-engine boundary has the opposite constraints. Nobody's browser talks to it directly, so human-readability buys nothing, but two languages now have to agree byte-for-byte on what a "resolve this poker hand" request looks like — and that's exactly the kind of contract that rots silently over a hand-maintained JSON schema shared across two codebases. A field renamed on one side, an int vs. a float on the other, an optional field one side forgets to check: all invisible until a hand resolves incorrectly in production. gRPC and .proto files fix this by making the schema the single source of truth — protoc generates matching Go and C++ stubs from the same definition, so a breaking change to a message shape is a compile error on both sides of the boundary, not a runtime surprise.

What gRPC actually buys beyond type safety

Three concrete things, for this specific boundary. First, binary protobuf encoding over HTTP/2 is meaningfully cheaper per call than JSON-over-HTTP/1.1 when the backend is issuing a hand-evaluation or bet-resolution call for every action at every table — overhead that's irrelevant for a handful of page loads but adds up across concurrent tables. Second, HTTP/2 multiplexing means many concurrent calls from the Go backend to the C++ engine share one connection without head-of-line blocking, which matters once several tables are resolving hands in the same tick. Third, a .proto service definition documents itself — the contract lives in one file that's also the code, rather than in a wiki page that drifts out of sync with what the endpoints actually accept.

The actual lesson

Neither protocol is "better" in the abstract — REST won at the browser edge for the same reasons gRPC won at the internal edge: match the protocol to who has to read it and how often it runs. A public API that outside developers or a browser will consume should optimize for debuggability and ubiquity; a private, high-frequency, cross-language boundary should optimize for a contract the compiler enforces. It's the same protocol-boundary thinking behind the WebSocket hub's topic fan-out, and behind treating process boundaries as something to design deliberately rather than paper over.

← Back to the Transcendence Casino project