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
- Schema — what’s the shape, and how does it evolve?
- Transport — REST, gRPC, GraphQL, message queue?
- Sync vs async — request/response, fire-and-forget, eventual consistency?
- Idempotency — what happens on retry?
- Versioning — how do producer and consumer drift safely?
- Errors — typed, with semantics (retryable vs not)?
- 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
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Schema | OpenAPI (often skipped) | Protobuf (mandatory) | SDL (mandatory) |
| Encoding | JSON | Binary (protobuf) | JSON |
| Default transport | HTTP/1.1 | HTTP/2 | HTTP/1.1 |
| Streaming | SSE / WebSocket bolt-on | Native | Subscriptions (bolt-on) |
| Browser-friendly | Yes | No (needs proxy) | Yes |
| Discoverability | Curl + OpenAPI | reflection / .proto | Introspection |
| Best for | Public APIs | Internal RPC | Aggregating disparate sources for UI |