gRPC vs REST vs GraphQL — three API styles compared. Three vertical columns: REST (resource-oriented HTTP, JSON, OpenAPI, cache-friendly, but suffers over-fetching and N+1); gRPC (HTTP/2, Protobuf, .proto contract, codegen, bidi streaming, ideal for internal microservices, weak browser support); GraphQL (single endpoint, schema, client picks fields, no over/under-fetching, BFF for multiple frontends, but caching is hard and unbounded queries are a DoS vector). Plus a Hybrid block illustrating the Netflix/Twitter/Shopify pattern: REST public + gRPC internal + GraphQL BFF. Four scenarios animate same user-and-posts fetch in each protocol plus a hybrid production path. Includes 4 ADRs for when to pick each, and the hybrid trade-off.
Выбор протокола между сервисами — самое частое архитектурное решение, которое команда принимает на старте и месяцами переписывает, если ошиблась. REST, gRPC и GraphQL не конкурируют за «лучший»: у каждого своя ниша, и хороший архитектор знает, на каком ребре системы какой протокол выигрывает.
Главная развилка не «какой протокол лучше», а «где проходит граница и кто за ней живёт»:
Один протокол на все три ребра — это всегда компромисс не в свою пользу.
«REST — для публичных HTTP API. gRPC — для внутреннего service-to-service. GraphQL — для frontends, которым нужно гибко выбирать поля из множества entities одним запросом.»
Расширенно: смотрите на edge (границу), а не на систему целиком. Public edge → REST (cache, universal clients). Service mesh edge → gRPC (perf, contract, streaming). Client edge для богатого UI → GraphQL BFF поверх gRPC-сервисов. Это и есть hybrid-паттерн Netflix / Twitter / Shopify.
Ключевое отличие от «выбора БД»: протокол можно (и нужно) миксовать в одной системе. Это не религиозный выбор.
Три колонки слева направо демонстрируют один и тот же сценарий — «получить пользователя и его 5 постов» — тремя протоколами:
rest-api → Postgres. Простая схема, но в анимации видно цену: два round-trip (GET /users/42 + GET /users/42/posts) и over-fetching (50 колонок в ответе, UI использует 3).grpc-server → Postgres. Один unary RPC GetUserWithPosts, бинарный фрейм в ~10× меньше JSON, плюс бидирекциональный стрим для подписок.gql-server → users-svc + posts-svc. Один POST /graphql, клиент сам описывает дерево полей, сервер фанаутит резолверы по микросервисам.BFF + gRPC + REST с ADR-004 — это не четвёртый протокол, а способ комбинировать первые три на разных рёбрах одной системы.На каждой ноде-сервере прикреплён ADR (ADR-001..ADR-004) — клик по узлу раскрывает решение в формате context/decision/date.
Четыре анимации в плеере (выбираются pills внизу) показывают трейд-оффы вживую:
REST: CRUD + N+1. GET → SQL → over-fetch (50 полей, использовано 3) → второй GET за постами → N+1 на каждом отношении → PUT для обновления, кешируется через ETag. Видно цену простоты: запросы повторяются, ответы избыточны, но всё это «just works» в любом языке и кешируется CDN.
gRPC: unary + bidi streaming. Один RPC GetUserWithPosts(id=42) по .proto-контракту, бинарный protobuf-фрейм, потом открывается двунаправленный стрим SubscribeNotifications — сервер пушит события по той же TCP-сессии (HTTP/2 multiplex). Финальный шаг — красный: попытка позвать gRPC напрямую из браузера падает, нужен gRPC-Web прокси.
GraphQL: одна query, вложенные резолверы. Клиент шлёт query { user(id:42) { name posts { title } } }, сервер резолвит user через users-svc, posts через posts-svc с DataLoader-батчингом (иначе был бы N+1 на стороне сервера), возвращает ровно запрошенное дерево. Последние два шага — атака: запрос с глубиной 12 уровней вложенности отбивается cost-limit guard.
Hybrid в проде. Mobile SDK ходит в REST (универсально, CDN-кешируется) → REST-handler внутри диспатчит в user-service через gRPC (sub-ms в кластере) → возвращает наружу JSON. Параллельно web-app ходит в GraphQL BFF, который тоже фанаутит в gRPC-сервисы и собирает дерево под форму экрана. Каждый протокол на своём ребре.
Public APIs обслуживают тысячи неизвестных интеграторов. Stripe, GitHub, Twilio выбрали REST: human-readable JSON, отладка через curl, кеширование на CDN и в браузерах, нативная поддержка во всех языках. Цена — over-fetching (сервер возвращает 50 полей, клиент использует 3) и N+1 round-trip за вложенными данными.
Решение: REST по умолчанию для public/external API, stateless CRUD над ресурсами, mobile-клиентов и везде, где HTTP-кеш (CDN, ETag, Last-Modified) — часть бюджета производительности. OpenAPI для контракта + codegen. Версионирование через major version в URL только при неизбежных breaking changes; предпочитайте аддитивную эволюцию.
Внутренним микросервисам нужны три вещи, в которых JSON-over-HTTP/1.1 плох: строгий разделяемый контракт, быстрый wire format, двунаправленный стриминг. Protobuf в 5-10× меньше JSON, codegen порождает type-safe stubs во всех языках, HTTP/2 мультиплексирует много вызовов на одном соединении, встроенный стриминг закрывает chat/telemetry/live updates без второго протокола.
Решение: gRPC для service-to-service внутри кластера, polyglot-команд (где .proto становится single source of truth), high-throughput backends, и любого RPC, которому нужен server-streaming, client-streaming или bidi. Избегать для браузерных клиентов (gRPC-Web работает, но добавляет translating proxy и ломает гибкость стриминга).
Web-app, iOS-app и Android-app каждый нуждается в разном срезе user/posts/comments. С REST каждый экран либо over-fetch'ит, либо делает 5 round-trip. GraphQL даёт клиенту описать ровно то дерево, которое нужно, одним запросом; schema даёт strong typing и introspection. Trade-offs: HTTP-кеш исчезает (POST), N+1 переезжает на сервер (нужен DataLoader), unbounded queries — DoS-вектор без depth + cost limits.
Решение: GraphQL как Backend-For-Frontend перед множеством микросервисов, когда есть несколько гетерогенных клиентов с разной формой данных (web, mobile, IoT). Обязательно: query depth limit, query cost analysis, persisted queries в проде, DataLoader для батчинга. Не выставлять GraphQL как public API без rate-limiting по cost units (а не по requests).
Большие системы редко выбирают один протокол. Netflix выставляет REST наружу (CDN cache, third-party SDK), гоняет gRPC между внутренними сервисами (perf + streaming), использует GraphQL как BFF для приложений, чтобы iOS-команду не блокировал backend-релиз ради каждого UI-изменения.
Решение: трактовать выбор протокола как per-edge, а не per-system. Public edge = REST. Service mesh edge = gRPC. Client edge для богатых UI = GraphQL BFF перед gRPC-сервисами. tRPC оставить для fully-TypeScript single-team стека, где codegen — overkill.
query { users { posts { author { posts { author { ... } } } } } } и кладёт сервер. Всегда query depth + cost analysis + persisted queries.GET /api/getUser?id=42 — это RPC over HTTP, не REST. Resource-orientation (/users/42) — это не косметика: она даёт HTTP-кеш, ETag, единообразные verb'ы, идемпотентность PUT/DELETE./v1/, /v2/) — нельзя плавно мигрировать клиентов. Лучше аддитивная эволюция (новые поля опциональны, deprecated помечается заголовком) + version в Accept header только когда реально нет выхода.first/after (cursor-based) на любых list-полях, и enforce это на уровне резолвера..proto linter / breaking-change detector — добавил поле как required, выкатил, сломал всех клиентов. Используйте buf breaking или эквивалент в CI.Не используйте REST, если у вас:
Не используйте gRPC, если у вас:
.proto без дисциплины breaks ломает всех клиентов разом.Не используйте GraphQL, если у вас:
Не используйте tRPC, если у вас: