The verdict
Use REST for public, cacheable, resource-oriented APIs with broad client support. Use gRPC for low-latency, high-throughput internal service-to-service communication where both sides are yours. Use GraphQL when many different clients need to shape their own responses over a complex, connected graph and you want to eliminate over- and under-fetching. Most production systems mix them: gRPC between services, REST or GraphQL at the edge.
How to decide
- 1Who consumes it? Public/partners → REST or GraphQL. Your own services → gRPC.
- 2What are the latency and throughput needs on the hot path? Very high → gRPC.
- 3Do clients need to shape responses or avoid many round-trips over connected data? → GraphQL.
- 4How much do you rely on HTTP caching and CDNs? A lot → REST.
- 5Do you need streaming (server push, bidirectional)? → gRPC (or GraphQL subscriptions).
- 6It's rarely one answer: gRPC internally, REST/GraphQL at the edge is a common, defensible split.
The options
REST (HTTP + JSON)
Resources over HTTP verbs, human-readable JSON.
Best for
- Public and partner APIs
- Resource-oriented CRUD
- Anything that benefits from HTTP caching and ubiquity
Strengths
- Universal client support and tooling
- Leverages HTTP caching, status codes, and CDNs directly
- Simple to learn, debug, and document (OpenAPI)
Weaknesses
- Over-fetching and under-fetching; often needs multiple round-trips
- No enforced schema/contract unless you add one
- Chatty for complex, nested data
gRPC (HTTP/2 + Protobuf)
Binary, contract-first RPC between services.
Best for
- Internal microservice-to-microservice calls
- Low-latency, high-throughput paths
- Polyglot backends needing a strict shared contract
Strengths
- Compact binary payloads and HTTP/2 multiplexing — low latency
- Strong, versioned contracts via Protobuf; codegen in many languages
- First-class bidirectional streaming
Weaknesses
- Not natively callable from browsers (needs gRPC-Web + a proxy)
- Binary payloads are harder to debug by hand
- HTTP caching doesn't apply the way it does for REST
GraphQL
A typed query language; clients ask for exactly what they need.
Best for
- Many client types (web, iOS, Android) with different data needs
- Complex, connected data where fixed endpoints multiply
- Aggregating several backends behind one graph
Strengths
- Clients request exactly the fields they need — no over/under-fetching
- One request can traverse a graph that would take many REST calls
- Strong typed schema and introspection
Weaknesses
- HTTP caching is hard (usually POST to one endpoint)
- N+1 resolver problems and expensive/abusive queries need guarding
- More server complexity; a real learning curve
Trade-offs at a glance
| Dimension | REST (HTTP + JSON) | gRPC (HTTP/2 + Protobuf) | GraphQL |
|---|---|---|---|
| Transport | HTTP/1.1+ | HTTP/2 | HTTP (usually POST) |
| Payload | JSON (text) | Protobuf (binary) | JSON |
| Performance | Good | Best (binary, multiplexed) | Good; depends on resolvers |
| HTTP caching | Native, easy | Not really | Hard |
| Client flexibility | Fixed endpoints | Fixed methods | Client shapes the query |
| Streaming | Limited (SSE/WebSocket) | First-class bidirectional | Subscriptions |
| Browser-native | Yes | No (needs proxy) | Yes |
| Best consumer | Public / cacheable | Internal services | Many varied clients |
In the interview
During the API-design portion of a system-design interview, or directly as 'how would you expose this service?' The interviewer is checking whether you match the protocol to the consumer and the constraints.
What a Staff answer includes
A Staff answer matches protocol to consumer, latency, and caching — and names the operational cost of each: gRPC isn't browser-native without a proxy, GraphQL needs query-depth/cost limits and a caching strategy, REST over-fetches. It's comfortable proposing a mix rather than a single winner.
Follow-ups you should expect
- →How do you version each of these without breaking existing clients?
- →How do you cache a GraphQL API effectively?
- →How do you protect a GraphQL endpoint from expensive or malicious queries?
- →You picked gRPC internally — how does a browser client reach it?
Common mistakes
- ✕Reaching for GraphQL everywhere, then fighting HTTP caching and query-cost attacks.
- ✕Exposing gRPC directly to browsers instead of a gateway.
- ✕Ignoring schema evolution and backward compatibility (Protobuf field numbers, additive changes).
- ✕N+1 queries in GraphQL resolvers because there's no batching/dataloader.
Further reading
Related decisions
Synchronous vs Asynchronous Communication
Synchronous when the caller needs an immediate answer to proceed; asynchronous messaging to decouple services, absorb load, tolerate downstream failure, or fan out — at the cost of eventual consistency and more moving parts.
Monolith vs Microservices
Start with a modular monolith. Move to microservices only for a concrete driver — independent scaling, team autonomy, or fault isolation — and only with the ops maturity to run a distributed system.