2026-07-17 · 8 min
gRPC vs REST vs GraphQL: Enterprise Pilih Mana 2026
Setahun lalu, klien enterprise (logistik B2B Indonesia + integrasi banyak partner) decide arsitektur API untuk platform baru. Ada 3 pilihan di meja: gRPC, REST, GraphQL. Tim split opinion. Saya jalankan PoC paralel 4 minggu + observasi 6 bulan post-deploy mixed approach. Share angka, trade-off, dan rekomendasi konkret per use case.
Setup PoC
API yang sama (warehouse management — list inventory, create shipment, update tracking) diimplementasi 3 cara:
- REST: Spring Boot 3.4 + Spring Web, JSON, OpenAPI 3 spec.
- gRPC: Spring Boot 3.4 + grpc-spring-boot-starter, Protobuf 3.
- GraphQL: Spring Boot 3.4 + spring-graphql, schema-first.
Backend: Postgres 17, Redis 7. Same DB schema, same business logic. Bench dari client di same VPC.
Benchmark: 60 menit sustained load
Read-heavy scenario (list 20 inventory items, dengan related data)
| Metric | REST | gRPC | GraphQL |
|---|---|---|---|
| Throughput | 14.200 RPS | 28.400 RPS | 9.800 RPS |
| Latency p50 | 12ms | 4ms | 22ms |
| Latency p95 | 38ms | 11ms | 78ms |
| Latency p99 | 95ms | 24ms | 180ms |
| Bandwidth (payload) | 18 KB | 4 KB | 22 KB |
| CPU server (avg) | 38% | 28% | 52% |
gRPC menang besar di throughput + latency + bandwidth. GraphQL paling slow karena N+1 resolver issue (akan dibahas di bawah).
Write scenario (create shipment dengan 5 line items)
| Metric | REST | gRPC | GraphQL |
|---|---|---|---|
| Throughput | 8.400 RPS | 11.200 RPS | 6.800 RPS |
| Latency p50 | 24ms | 14ms | 32ms |
| Latency p95 | 80ms | 38ms | 110ms |
| Latency p99 | 180ms | 78ms | 220ms |
Write scenario gap lebih kecil karena DB write dominate latency budget, network/serialization less impactful.
Mobile client scenario (slow 3G simulation, 500ms latency)
| Metric | REST | gRPC | GraphQL |
|---|---|---|---|
| Time to first byte | 612ms | 580ms | 645ms |
| Total round trips for 3 related queries | 3 | 3 (or 1 streaming) | 1 |
| Total data fetched | 54 KB | 12 KB | 26 KB |
GraphQL menang di round trip count + flexibility. gRPC menang di payload size. Untuk mobile dengan latency tinggi (Indonesia 3G/4G rural): GraphQL lebih responsif kalau query kompleks.
Dev experience comparison
REST
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
@GetMapping("/{id}")
public InventoryResponse getInventory(@PathVariable String id) {
return inventoryService.findById(id)
.map(InventoryResponse::from)
.orElseThrow(() -> new NotFoundException(id));
}
}
OpenAPI 3 spec auto-generated via SpringDoc. Familiar untuk junior dev. Client SDK generate via openapi-generator (Kotlin, TypeScript, Python).
Onboarding: junior dev produktif < 1 minggu.
gRPC
inventory.proto:
syntax = "proto3";
package warehouse.v1;
service InventoryService {
rpc GetInventory(GetInventoryRequest) returns (Inventory);
rpc ListInventory(ListInventoryRequest) returns (stream Inventory);
rpc UpdateStock(UpdateStockRequest) returns (UpdateStockResponse);
}
message GetInventoryRequest {
string id = 1;
}
message Inventory {
string id = 1;
string sku = 2;
int32 quantity = 3;
Location location = 4;
google.protobuf.Timestamp last_updated = 5;
}
Generated stub via protoc:
val inventory = inventoryStub.getInventory(
GetInventoryRequest.newBuilder().setId(id).build()
)
Type-safe, fast (binary protocol), bidirectional streaming native.
Onboarding: junior dev butuh 2-3 minggu (protoc tooling, schema evolution, stream concept).
GraphQL
Schema:
type Inventory {
id: ID!
sku: String!
quantity: Int!
location: Location!
lastUpdated: DateTime!
}
type Query {
inventory(id: ID!): Inventory
inventories(filter: InventoryFilter, limit: Int = 20): [Inventory!]!
}
type Mutation {
updateStock(id: ID!, delta: Int!): Inventory!
}
Resolver:
@Controller
public class InventoryResolver {
@QueryMapping
public Inventory inventory(@Argument String id) {
return inventoryService.findById(id);
}
@SchemaMapping(typeName = "Inventory")
public Location location(Inventory inventory) {
return locationService.findById(inventory.getLocationId()); // N+1 trap!
}
}
N+1 fix via DataLoader:
@BatchMapping(typeName = "Inventory")
public Map<Inventory, Location> location(List<Inventory> inventories) {
Set<String> locationIds = inventories.stream()
.map(Inventory::getLocationId).collect(Collectors.toSet());
Map<String, Location> locationMap = locationService.findAllById(locationIds);
return inventories.stream()
.collect(Collectors.toMap(i -> i, i -> locationMap.get(i.getLocationId())));
}
Onboarding: junior dev butuh 4-6 minggu (resolver pattern, DataLoader, N+1 awareness, schema federation).
Schema evolution
REST
Backward-compatible change (add optional field): no client break.
Breaking change: version in URL (/v2/inventory).
Lifecycle: documented, sunset notification, dual-version support 6-12 bulan.
Mature ecosystem, simple semantics.
gRPC
Protobuf rules:
- Add field: must be optional (Proto3 default), reserved field number, new field higher number. No break.
- Rename field: don’t, jangan pernah. Pakai
reservedkeyword untuk old name. - Change field type: no, breaking. Add new field instead.
Strict discipline, but predictable. Tooling (buf, protolint) catch most violations CI-time.
GraphQL
Add field: backward-compatible (client opt-in via query).
Remove field: deprecate first via @deprecated, sunset after grace period.
Breaking change: schema federation kompleks, careful coordination cross-team.
Schema flexible but coordination overhead di organisasi besar.
Yang dipilih untuk klien
Setelah PoC + 6 minggu analisis tim:
| Use case | Pilihan | Alasan |
|---|---|---|
| Service-to-service internal | gRPC | Throughput tertinggi, type-safe, streaming |
| Public API for partners (15 partner) | REST | Universal, OpenAPI familiar, partner support |
| Mobile app (iOS + Android) | gRPC | Native SDK, bandwidth efisien, streaming |
| Web SPA (admin dashboard) | GraphQL | Multiple data shape per page, flexible |
| Webhook outgoing (notify partner) | REST | Standar, partner accept |
| Integration with legacy SAP | REST | SAP integration native REST |
Mixed approach. Bukan “satu pilihan untuk semua”. Pertimbangan organizational: tim mobile berbeda dari tim web, kebutuhan beda.
Implementation di production
gRPC service-to-service
Setup Envoy sidecar untuk:
- TLS termination internal
- Authentication via mTLS
- Observability (gRPC-aware metrics + tracing via OTel)
- Retry + circuit breaker
- Load balancing client-side (gRPC native LB)
# envoy sidecar config snippet
clusters:
- name: inventory-service
type: STRICT_DNS
connect_timeout: 0.5s
http2_protocol_options: {}
load_assignment:
cluster_name: inventory-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: inventory-service.internal
port_value: 9090
health_checks:
- timeout: 1s
interval: 10s
unhealthy_threshold: 3
healthy_threshold: 2
grpc_health_check: {}
REST public API gateway
Spring Cloud Gateway + OpenAPI:
- Rate limit per API key (1.000 req/min default, tier-based)
- API key + JWT validation (OAuth 2.1 dari OAuth PKCE Spring)
- Request logging untuk audit
- Versioning via URL path
GraphQL gateway (admin dashboard)
Spring GraphQL + DataLoader pattern. Schema federation tidak dipakai (overhead tidak worth untuk single backend).
Setup:
- N+1 prevention via DataLoader (mandatory di code review).
- Query complexity analysis (reject query depth > 7).
- Persisted queries (production: only allowlisted queries, no arbitrary).
- Caching via Apollo Server-side (planned, belum implement).
Hasil 6 bulan post-deploy
| Metric | Before (monolith REST) | After (mixed) |
|---|---|---|
| Service-to-service p99 | 180ms | 38ms (gRPC) |
| Mobile API bandwidth | 100% | 28% (gRPC) |
| Admin dashboard page load | 2.4s | 0.9s (GraphQL) |
| Partner integration time | 6-8 weeks | 4-5 weeks (REST + OpenAPI) |
| Developer onboarding | 2-3 weeks | 3-5 weeks (mixed) |
| Operational complexity | low | medium-high |
Latency win untuk gRPC service-to-service signifikan. Admin dashboard page load win karena GraphQL single round trip vs REST multiple call.
Yang break
1. gRPC client tidak compatible saat schema field deprecated
Field legacy_id di-deprecate di v2. Mobile app versi lama tetap pakai. Saat field di-remove dari protobuf, mobile app crash deserialize.
Fix: jangan pernah remove field, hanya reserved. Plus client version tracking, sunset old client version > 6 bulan.
2. GraphQL N+1 issue di production
DataLoader implementation di-lupakan di beberapa resolver. Single GraphQL query trigger 80+ DB query.
Detection: alert db.query.count > 30 per request. Investigate, fix DataLoader.
Pasca insiden: pre-commit hook yang check pattern resolver tanpa DataLoader.
3. REST API versioning kompleks
V1 + V2 coexist. Dokumentasi 2 versi, bug fix harus apply ke 2. Bandwidth tim hilang untuk maintenance dual-version.
Fix: hardcap V1 deprecation 12 bulan post-V2 launch. Communicate explicit ke partner.
4. gRPC-Web limitation di browser
Saya pernah pertimbangkan gRPC-Web untuk admin dashboard (single backend untuk web + mobile + internal). gRPC-Web ada limitation (no server streaming yang full, no bidirectional streaming).
Fix: stick dengan GraphQL untuk web. Mixed protocol OK.
5. Schema evolution coordination
3 protocol = 3 schema/spec. Sometimes change di domain model harus propagate ke 3 tempat (proto + OpenAPI + GraphQL schema).
Fix: single source of truth di domain model Java, dengan generator yang produce 3 protocol spec. Maintain 1 file, generate 3.
# Maven plugin generate proto + OpenAPI + GraphQL schema dari Java annotations
./mvnw schema:generate-all
Verdict
Tidak ada protocol “terbaik” untuk semua. Per use case:
- gRPC: service-to-service backend (terutama high-throughput), mobile native client, streaming workload.
- REST: public API for partners, integration legacy/3rd party, simple CRUD.
- GraphQL: dashboard SPA dengan complex data shape per page, multiple client dengan kebutuhan data berbeda.
Untuk enterprise Indonesia: mixed approach itu mature engineering decision, bukan indecisiveness. Yang penting: clear ownership per protocol, dokumentasi standardize, dan tim siap maintain multi-protocol stack.
Bukan magic. Mixed approach butuh tim yang familiar 3 protocol, tooling investment, dan disiplin schema evolution. Untuk SMB: pilih 1 protocol dominan, kompromi performance untuk simplicity.
Lihat juga Spring Boot modular monolith untuk konteks bagaimana protocol choice intersect dengan service boundary architecture.
Ditulis oleh Reza Pradipta