Concept
When a software system is composed of multiple services, those services need to exchange data. The two dominant styles are REST over HTTP and gRPC over HTTP/2. Each has a distinct design philosophy, set of trade-offs, and appropriate use cases.
Problem
Service communication introduces failure modes that don't exist in a single process. The network can be slow, unavailable, or return partial responses. A caller doesn't know if a slow response means the server is processing or has failed. A request might arrive at the server but the response might be lost — meaning a retry could cause a duplicate operation.
These are the fundamental challenges: latency, partial failure, and the need for services to remain useful even when their dependencies are degraded.
How it works
REST (Representational State Transfer) uses HTTP methods and URLs to model resources and actions. Data is typically exchanged as JSON. REST is human-readable, easy to explore with a browser or curl, and widely supported by tooling and infrastructure.
REST's weakness is that its contract is informal. An API's structure is documented in prose or an OpenAPI spec, but there's no built-in enforcement at the protocol level. Clients and servers can drift out of sync.
gRPC uses Protocol Buffers to define a service contract in a
.proto file. The toolchain generates client and server code from
this definition, ensuring both sides agree on the message structure.
gRPC uses HTTP/2 for transport, which enables multiplexed streams and
bidirectional communication.
gRPC is efficient (binary encoding, smaller payloads than JSON), strongly typed (the generated code enforces the contract), and well-suited for internal service communication. It is harder to inspect than REST because the payload is binary.
Trade-offs
REST works well for public-facing APIs where human readability and broad client compatibility matter. JSON is self-describing, easy to log, and understood by virtually every language and framework.
gRPC works well for internal service-to-service communication where performance, strong typing, and code generation are more valuable than human readability. The generated client and server stubs reduce the risk of contract mismatches.
For small systems and monoliths, neither is necessary. The complexity of service boundaries and a communication protocol only pays off when the system is actually split across deployment units.
Practical application
Simple Bank uses REST for its external API — clients are external and JSON over HTTP is the right choice. RentLoop communicates with the WhatsApp API using webhooks (incoming HTTP POST requests) and outgoing REST calls — a model where the external service pushes events rather than the application polling.
Key practices regardless of protocol:
- Set timeouts. Every outgoing request needs a timeout. A slow dependency should not block a goroutine indefinitely.
- Handle retries carefully. Retrying a failed request is often appropriate, but only if the operation is idempotent. A failed transfer request should not be retried blindly — it might have succeeded and the response was simply lost.
-
Propagate context. Pass
context.Contextinto every outgoing request so cancellations and deadlines propagate through the call chain.
My understanding
The most important insight is that network communication is not just a function call that happens over the wire. A function call either returns or panics. A network call can timeout, return an error, return a partial response, or succeed without you knowing it succeeded. The semantics are fundamentally different.
I've found that building RentLoop — which depends on the WhatsApp API being available and responding correctly — forced me to think about what happens when it doesn't. Good service communication design starts with the question: what does my service do when a dependency is unavailable or slow?