System Design · Synchronous Communication
REST vs. RPC
Nouns-and-verbs versus named functions — why most 'REST' APIs are really RPC in a resource-flavored costume, and when to stop pretending.
Ask two engineers to design an API for the same feature and you'll often get two different mental models before a single line of code is written. One reaches for GET /orders/42 and POST /orders. The other writes a function called cancelOrder(orderId) and calls it a day. Neither is wrong — they're just answering a different question. REST asks "what resource am I acting on, and which of a small, fixed set of verbs applies?" RPC asks "what operation do I want to perform?" and lets you name it whatever you like.
Nouns versus verbs
REST is fundamentally resource-oriented. Everything in the system — a user, an order, a comment — is a noun addressed by a URL, and you manipulate it through a uniform, deliberately small set of HTTP methods: GET to read, POST to create, PUT/PATCH to update, DELETE to remove. The API surface grows by adding resources, not by adding verbs — /orders/42/cancel is already a slightly awkward compromise, because "cancel" isn't one of the five verbs REST gives you.
RPC flips that around. The unit of design is the procedure — a named function you call remotely, with whatever inputs and outputs the operation needs. transferFunds(from, to, amount) doesn't need to pretend it's creating or updating a resource called "transfer"; it just does what it says. The API surface grows by adding functions, and there's no philosophical tension in calling one mergeAccounts and the next recalculateShippingRates.
The parts of REST almost nobody implements
Roy Fielding's original dissertation describes REST as more than "URLs plus HTTP verbs." Two of its core constraints are statelessness — every request carries all the context needed to process it, with no server-side session between calls — and HATEOAS (Hypermedia As The Engine Of Application State): responses should include links describing what you're allowed to do next, so a client discovers the API by following links rather than hardcoding URL templates ahead of time.
Be honest about what you're building
Statelessness caught on broadly. HATEOAS, almost never. A response that includes _links.cancel.href and expects the client to navigate from there is rare in production — most teams hardcode PATCH /orders/{id} directly into their client code, which is exactly the coupling HATEOAS exists to avoid. What most people call "REST" is really HTTP + JSON RPC wearing a resource-flavored URL scheme — and that's a perfectly fine thing to build, as long as you don't confuse it with the original ideal.
Where RPC genuinely wins
Some operations resist the resource model no matter how creatively you route them. "Transfer $500 from account A to account B" isn't really a create, read, update, or delete on any single resource — it's a transaction spanning two resources with business rules (sufficient balance, fraud checks, double-entry bookkeeping) that don't live comfortably behind a PATCH. "Merge these two duplicate customer records" is similar: which resource owns that action, and what HTTP verb means "merge"? RPC just lets you name the operation and move on.
service AccountService {
// A verb the resource model has no room for.
rpc TransferFunds (TransferRequest) returns (TransferResponse);
}
message TransferRequest {
string from_account_id = 1;
string to_account_id = 2;
int64 amount_cents = 3;
string idempotency_key = 4;
}This is also why RPC dominates internal, service-to-service communication. gRPC — built on HTTP/2 and Protocol Buffers — gives you strongly typed contracts, code generation in every language, and low-overhead binary payloads, all of which matter more when you control both ends of the call and are optimizing for throughput between your own microservices rather than for a third-party developer poking around with curl. Older RPC styles (XML-RPC, SOAP) solved the same problem with heavier, more verbose envelopes, but the motivating idea — call a named operation, get a typed result — hasn't changed.
Versioning: two different philosophies
REST APIs typically version at the boundary: a path prefix like /v2/orders, or content negotiation via an Accept header (application/vnd.myapi.v2+json). Either way, a new version is a coarse-grained fork — clients pin to one version, and the server maintains multiple versions side by side until old clients migrate off.
gRPC and other schema-driven RPC systems favor gradual schema evolution instead. Protobuf field numbers are permanent and additive: you add new optional fields, never renumber or repurpose existing ones, and old clients simply ignore fields they don't recognize while old servers leave new fields unset. Done carefully, this gives you backward and forward compatibility without ever minting a "v2" — the wire format evolves field by field instead of forking wholesale.
Pick the versioning story that matches your blast radius
Coarse URL versioning is easier to reason about when you have many external clients you can't coordinate with — everyone knows exactly which contract they're on. Schema evolution shines internally, where dozens of services redeploy independently and you'd rather never have a "v2" migration at all.
The pragmatic middle ground
In practice, almost no production API is purely one or the other. The CRUD-shaped parts of a domain — orders, users, invoices — get resource-ish URLs and standard verbs, because that mapping is genuinely natural and gives you free wins: caching semantics on GET, idempotency on PUT, a predictable shape for every client that's ever seen a REST API before. The handful of operations that don't fit — actions, workflows, anything transactional or cross-resource — get RPC-style action endpoints (POST /orders/{id}:cancel, POST /accounts:transfer) rather than being forced into a verb that doesn't fit. Trying to be dogmatically pure about either style usually costs more in awkward workarounds than it buys in architectural cleanliness.
Key takeaways
- REST organizes an API around resources (nouns) and a fixed verb set; RPC organizes it around named operations (verbs).
- Statelessness stuck; HATEOAS mostly didn't — most 'REST' APIs are really HTTP+JSON RPC with resource-shaped URLs.
- RPC fits operations that don't map to CRUD (transfers, merges) and internal service-to-service calls where you control both ends.
- REST typically versions at the boundary (URL or content negotiation); schema-driven RPC like gRPC evolves field by field instead.
- Most production APIs blend both: resource endpoints for CRUD, action-style RPC endpoints for everything that doesn't fit that mold.
