You're designing a new service and someone asks: "Should we use REST, GraphQL, or gRPC?" The honest answer depends on the problem — but "it depends" without a framework for deciding is useless. Here are the real trade-offs based on how these technologies behave at runtime, not just in documentation.
All three are solid choices for different problems. The mistake is treating them as competitors when they're usually complements: REST for public APIs, gRPC for internal services, GraphQL for client-driven data needs.
REST maps HTTP verbs to resources. GET /orders/42 fetches an order. The contract is implicit, documented separately. Data travels as JSON over HTTP/1.1.
gRPC defines a strict binary contract upfront in .proto files, generates client and server code in any supported language, and transmits compact binary frames over HTTP/2. Calling a remote service looks like a local method call.
GraphQL exposes a single endpoint and lets the client describe exactly what fields it wants. The server holds a typed schema; the client sends a query document and gets back exactly the shape it asked for.
REST gets a bad reputation from developers who've moved on, but it remains the best default for public APIs, webhooks, and any service needing broad client compatibility.
HTTP caching works out of the box — CDNs, proxies, and browsers all understand Cache-Control and ETags natively. Any HTTP client in any language can consume a REST API without generated stubs or special libraries.
The problems come at scale with over-fetching and under-fetching. To render a user dashboard you might need:
GET /users/42 # user profile GET /orders?userId=42&limit=5 # recent orders GET /accounts/42/balance # balance
Three round trips. Each returns more data than the screen needs. You can work around it with purpose-built aggregate endpoints (GET /users/42/dashboard) but now your backend is shaped around one client's needs — you'll be updating it every time the mobile UI changes.
Use REST when: the API is public or consumed by third parties, clients are diverse, HTTP caching is important, or the team values familiarity.
gRPC was built for high-throughput, low-latency communication between internal services where you control both ends.
The performance case is concrete. Protobuf binary serialization is ~3–10x faster than JSON, with payloads ~60–80% smaller. Atlassian measured Protobuf data being ~80% smaller than JSON when migrating Jira Cloud's internal APIs. At thousands of inter-service calls per second, that compounds significantly.
The contract is strict by design. You define a .proto file, run mvn compile, and get type-safe client and server stubs automatically:
service OrderService { rpc GetOrder (GetOrderRequest) returns (OrderResponse); rpc StreamOrderUpdates (GetOrderRequest) returns (stream OrderEvent); } message GetOrderRequest { int64 order_id = 1; } message OrderResponse { int64 order_id = 1; string status = 2; double total = 3; }
That .proto generates a Java stub your services call like a local method:
// Calling the order service from another service OrderResponse order = orderServiceStub.getOrder( GetOrderRequest.newBuilder().setOrderId(42).build() );
Bidirectional streaming is the other killer feature. gRPC supports four stream types over a single persistent HTTP/2 connection. For real-time data pipelines or live monitoring feeds, this is significantly cleaner than polling or WebSocket workarounds.
The real limitation is browsers. They can't speak HTTP/2 trailers which gRPC needs for metadata. You need a grpc-web proxy (typically Envoy) to reach gRPC from the browser — extra operational complexity that's usually not worth it for browser-facing APIs.
Use gRPC when: performance is critical, both ends are under your control, you need streaming, or services are polyglot (gRPC is language-agnostic).
📚 Related: gRPC with Java and Spring Boot — full walkthrough with all four streaming types.
GraphQL solves a specific problem: when multiple clients (iOS, Android, web, dashboard) need the same underlying data in different shapes, REST forces you to either over-fetch or build client-specific endpoints.
A single GraphQL query can fetch what would require three REST calls — with exactly the fields the client uses:
query GetUserDashboard($userId: ID!) { user(id: $userId) { name email recentOrders(limit: 5) { id total status } accountBalance } }
The mobile client asks for name, email, and 5 orders. The web dashboard asks for those plus full order history. Same endpoint, same schema, different queries.
Schema evolution is cleaner too. Adding a field is non-breaking — existing clients just don't query for it. Compare this to REST versioning (/v1/, /v2/) where a breaking change forces you to maintain two API versions.
Where GraphQL gets painful is caching. REST GET requests cache trivially at the HTTP layer. GraphQL typically uses POST for queries, so HTTP caches are blind. You need application-level caching and DataLoader for batching N+1 resolver calls.
The N+1 problem is particularly nasty. Each nested resolver runs separately per parent item:
# Without DataLoader this hits the DB once per user query { users { # returns 100 users recentOrders { # executes 100 separate queries total } } }
Security also needs more upfront work. A deeply nested query can be expensive. You need query depth limiting and complexity analysis from day one.
Use GraphQL when: multiple client types need the same data differently, you're building a BFF layer, or frontend teams want autonomy over data queries.
📚 Related: GraphQL with Java and Spring Boot — complete implementation with subscriptions and DataLoader.
In practice, the best architectures don't pick one — they use each where it fits:
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Public API | ✅ Best choice | ❌ Browser issues | ⚠️ Complexity overhead |
| Internal services | ⚠️ Works but slow | ✅ Best choice | ❌ Over-engineered |
| Multiple client shapes | ⚠️ Over-fetching | ❌ Not designed for it | ✅ Best choice |
| Real-time streaming | ⚠️ SSE/WebSocket bolt-on | ✅ Native | ⚠️ Subscriptions |
| Mobile bandwidth | ⚠️ Over-fetching | ✅ Smallest payloads | ✅ Precise queries |
| HTTP caching | ✅ Native | ❌ Manual | ❌ POST-based |
Quick rules:
Learn the fundamentals of GraphQL and how to build GraphQL APIs in Java and Spring Boot with an event booking application.
Learn the basics of gRPC and how to build gRPC microservices in Java and Spring Boot.
This guide explores the features and usage of the RestClient introduced in Spring 6, providing a modern and fluent API for making HTTP requests. It demonstrates how to create and customize RestClient instances, make API calls, and handle responses effectively.
Learn how to effectively version your Spring Boot APIs. This guide covers common versioning strategies, their implementation in Spring Boot, and best practices.
Find the most popular YouTube creators in tech categories like AI, Java, JavaScript, Python, .NET, and developer conferences. Perfect for learning, inspiration, and staying updated with the best tech content.

Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.