Microservices buy you independent deploys, independent scaling, and team autonomy. They cost you a distributed system. The decision is rarely “monolith or microservices” — it’s “where do the seams go and what’s the cost of the wrong cut.”

When microservices pay for themselves

  • Multiple teams need to ship on different cadences without coordinating.
  • Components have wildly different scaling profiles (an inference service vs a CRUD admin) or runtime requirements (GPU vs CPU).
  • A subsystem has a different consistency / durability profile than the rest.
  • A subsystem has different blast-radius requirements (fault isolation, security).

When none of those apply, a modular monolith with strict internal interfaces is faster to build and easier to operate. “We’ll microservice it later” is fine — the seams you draw inside a monolith become service boundaries when you actually need them.

The contract questions every cross-service call has to answer

  1. Schema — what’s the shape, and how does it evolve?
  2. Transport — REST, gRPC, GraphQL, message queue?
  3. Sync vs async — request/response, fire-and-forget, eventual consistency?
  4. Idempotency — what happens on retry?
  5. Versioning — how do producer and consumer drift safely?
  6. Errors — typed, with semantics (retryable vs not)?
  7. Auth & tenancy — propagated how?

Skipping any of these is how distributed monoliths happen.

gRPC, in one screen

gRPC is Protobuf schemas + HTTP/2 transport + generated clients/servers in your language. What you actually get:

  • Strongly typed contracts that fail at compile time, not at 3am.
  • Schema evolution rules baked in — add fields with new tag numbers, never reuse, never change types. Old clients ignore unknown fields.
  • Efficient binary encoding — typically 3–10× smaller than JSON, faster to parse.
  • Streaming — server-streaming, client-streaming, bidi — without inventing your own framing.
  • Code generation for ~10 languages, so polyglot service meshes work without writing clients by hand.
syntax = "proto3";
 
message GreetRequest { string name = 1; }
message GreetResponse { string message = 1; }
 
service Greeter {
  rpc Greet(GreetRequest) returns (GreetResponse);
}

When gRPC is the right call

  • Internal service-to-service traffic at scale.
  • Polyglot environments where hand-written REST clients are a tax.
  • Streaming workloads (logs, metrics, real-time updates).
  • You have control of both ends.

When it’s not

  • Browser clients. gRPC-web exists, but it requires a proxy and loses HTTP/2’s nicer features. Use REST/JSON or GraphQL on the edge, gRPC behind it.
  • Public APIs. REST/JSON is the lingua franca; consumers won’t tolerate a custom toolchain.
  • One service, one client. The schema upkeep tax isn’t worth it; JSON over HTTP is fine.

REST vs gRPC vs GraphQL — the actual decision

RESTgRPCGraphQL
SchemaOpenAPI (often skipped)Protobuf (mandatory)SDL (mandatory)
EncodingJSONBinary (protobuf)JSON
Default transportHTTP/1.1HTTP/2HTTP/1.1
StreamingSSE / WebSocket bolt-onNativeSubscriptions (bolt-on)
Browser-friendlyYesNo (needs proxy)Yes
DiscoverabilityCurl + OpenAPIreflection / .protoIntrospection
Best forPublic APIsInternal RPCAggregating disparate sources for UI