</> MAANG.io
system design Β· 101

Foundations

Master system design interviews with scalable architecture patterns, distributed systems, and real-world design challenges.

0/81 solved 0% complete

Part 1 β€” Foundations of RPC (Remote Procedure Call)

(maang.io System Design Series)

What is this?

Imagine you're at your desk and you need a number crunched. You could do it yourself β€” or you could pick up the phone, call a colleague in another office, say "compute this for me," and a moment later they read the answer back. You wrote no network code, opened no socket, parsed no bytes. From where you sit, it felt almost exactly like calling a function. You said the name of the task, you handed over the inputs, you got a result.

That's a Remote Procedure Call. You call what looks like an ordinary local function β€” getUser(42) β€” and somewhere behind the scenes that call is packed up, shipped across the network to another machine, executed there, and the result is shipped back and handed to you as a normal return value. A piece of generated code called a stub (or proxy) sits between you and the network and performs the magic trick: it makes a call to another computer look like a call to a function in your own program.

RPC call flow: a local-looking call marshalled across the network and back

The one-line idea: RPC lets you call a function that secretly runs on another machine, hiding the network behind a local-looking call β€” but the network is not actually invisible, and pretending it is will eventually bite you.

That second half of the sentence is the soul of this whole topic. The stub gives you a beautiful illusion: "the network is just a function call." But it's an illusion, and a leaky one. A local function call never times out, never half-completes, never costs 80 milliseconds, never silently vanishes because a switch rebooted. A remote one does all of those things. Learning RPC well means enjoying the convenience of the illusion while never trusting it. We'll earn that lesson across these four parts, and it ties directly to networking failure modes and the CAP theorem β€” when the network breaks, your "function call" is the thing standing in the breach.


1. Why RPC exists

Step back to the problem RPC was invented to solve. You've split your application into services β€” a user service, an order service, a payment service β€” and now they have to talk to each other constantly. Service-to-service chatter is the inner loop of a microservice system; a single user request might fan out into dozens of internal calls.

You could make every one of those internal calls a hand-rolled HTTP request: build a URL, serialize a JSON body, set headers, send it, parse the response, check the status code, map errors. That's fine once. It's miserable a thousand times, across a dozen teams, in five languages. Three things go wrong at scale:

  • Boilerplate everywhere. Every caller re-implements the same serialize/send/parse/error-handle dance, and every implementation drifts a little.
  • No shared contract. The order service thinks the user service returns a field called email. Nobody enforces that. A rename ships, and things break at runtime, in production, far from the change.
  • The text tax. JSON is verbose and string-typed. Encoding numbers as decimal text, re-parsing field names on every message, shipping 2–3Γ— more bytes than you need β€” it adds up fast when you're doing it millions of times a second between machines in the same data center.

RPC attacks all three. You write the contract once in a schema, a tool generates the client and server code so nobody hand-rolls anything, and the data travels in a compact binary form. The result is internal communication that is fast, structured, type-checked, and boring β€” which is exactly what you want from plumbing. This is why gRPC (Google), Thrift (Meta), Finagle (Twitter), and Dubbo (Alibaba) carry the internal traffic of some of the largest systems on earth. We dig into gRPC β€” the dominant modern RPC framework β€” in its own chapter.


2. How an RPC actually works: the full call flow

This is the mechanism you must be able to draw on a whiteboard. Let's trace a single call, getUser(42), from the client's stack frame all the way to the server and back. Six moving parts: the stub, marshalling, the transport, the skeleton, unmarshalling, and the actual function.

Sequence of a full RPC call: app, stub, network, skeleton, real implementation and back

Walking it slowly, because each step is a place where things can leak:

  1. The call. Your code invokes getUser(42). But getUser here isn't the real implementation β€” it's the stub, a generated stand-in with the same signature. It accepts your call so the rest of your code never knows the difference.
  2. Marshalling. The stub takes the method name and the argument 42 and serializes them into a compact byte string β€” a process called marshalling (or serialization). "Turn live objects into a flat sequence of bytes I can put on a wire."
  3. Transport. Those bytes go out over the network β€” typically TCP, and for modern frameworks, framed inside HTTP/2 (more on transports in Part 2).
  4. Dispatch. On the server, the skeleton (the server-side stub, sometimes called the dispatcher) receives the bytes, unmarshals them back into a method name and arguments, and figures out which real function to call.
  5. Execution. The skeleton invokes the actual getUser(42) β€” real business logic, real database query β€” and gets a User back.
  6. The return trip. The result is marshalled, sent back across the network, unmarshalled by the client stub, and handed to your code as the return value of the call you made in step 1.

πŸ’‘ Here's the mental model to lock in: the stub and skeleton are mirror images, and marshalling/unmarshalling is a matched pair on each end. The whole apparatus exists to convert "a function call" into "bytes on a wire" and back, on both sides, automatically. You write none of it β€” but you should be able to picture all of it, because every failure mode lives in one of these steps.


3. The Interface Definition Language (IDL): the contract

How do the client and server agree on what getUser looks like β€” its arguments, its return type, what counts as a valid request? They don't negotiate it at runtime. They share a single source of truth written ahead of time in an Interface Definition Language (IDL).

An IDL is a small, language-neutral schema that describes your services and messages. Here's the flavor, in Protocol Buffers (gRPC's IDL):

message GetUserRequest { int64 user_id = 1; }
message User { int64 id = 1; string name = 2; string email = 3; }

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
}

That tiny file is the contract. You run it through a code generator and out come the stubs β€” a client library and a server interface β€” in whatever languages you need:

IDL contract compiled into client stubs and a server skeleton

This is the part that makes RPC feel magical and safe. Because the contract is generated, not hand-written:

  • Types are checked at compile time. Pass a string where a user_id should be an int64 and your build fails β€” not your 2 a.m. pager.
  • Languages stop mattering. A Go service and a Java service that share a .proto can call each other flawlessly; each just generates a stub in its own language.
  • The schema can evolve safely. Notice the = 1, = 2, = 3 β€” those are field numbers, not positions. The wire format identifies fields by number, so you can add new fields or reorder lines without breaking old clients. (We cover schema evolution properly in Part 2; it's one of RPC's superpowers.)

The big IDL/serialization families you'll hear named: Protocol Buffers (Google, the default for gRPC), Apache Thrift (Meta), and Apache Avro (the data/streaming world, prized for schema evolution). All three do the same core job β€” one contract, many generated stubs, compact binary on the wire.


4. The leaky abstraction: the network is not invisible

Now the most important section in this chapter, the one that separates people who use RPC from people who understand it. The stub's job is to make a remote call look local. The danger is believing it.

A local function call has properties you've relied on your whole career without noticing: it always returns (or throws) promptly, it never half-runs, it costs nanoseconds, and "I called it" and "it executed" are the same event. A remote call quietly breaks every one of those assumptions. This is the famous list known as the Fallacies of Distributed Computing β€” the things engineers assume about networks that are all false:

The eight Fallacies of Distributed Computing, all false

The two that bite RPC hardest:

  • Latency is not zero. A local call is essentially free. A cross-machine call is hundreds of microseconds at best; a cross-region call is 100+ milliseconds. Code that loops calling a remote function "just like a local one" β€” the classic chatty interface β€” can be thousands of times slower than the developer imagined. The convenient illusion hides a real cost.
  • The network is not reliable β€” which means partial failure. This is the deep one. When a local call fails, it fails completely and visibly β€” you get an exception, and nothing happened. When a remote call fails, you can land in a terrifying middle state: you don't know whether it ran.

Look closely at where a call can die, because the ambiguity is the whole problem:

Timeout ambiguity: the client cannot tell whether the request ran

You sent chargeCard($50) and the call timed out. Did the charge happen? You cannot know. Maybe the request never reached the server (safe to retry). Maybe the server charged the card and the response got lost on the way back (retrying double-charges the customer). A timeout is not "it failed" β€” it's "I don't know," and "I don't know" is the hardest state in all of distributed systems. It's the same ambiguity at the heart of the CAP theorem: a silent network looks identical to a dead peer.

⚠️ The takeaway: never write RPC code that assumes a call either fully succeeded or fully failed. It can also do the worst thing β€” succeed on the server while appearing to fail to the client. Designing around that ambiguity is what Part 3 calls delivery semantics, and it's where idempotency becomes your best friend.


5. Synchronous vs. asynchronous RPC

There's a second design axis: when you make the call, does your thread wait?

Synchronous vs asynchronous RPC

  • Synchronous RPC is the default and the most natural: you call, your thread blocks until the answer comes back, then you continue. It reads like ordinary code, which is the point. The cost is a held resource β€” a thread or connection sits idle waiting. Under load, or in a deep chain of calls (A waits on B waits on C), blocked threads pile up and you risk thread-pool exhaustion and cascading timeout stacking.
  • Asynchronous RPC doesn't block. The call returns a Future/Promise (a placeholder for "the answer, eventually") right away, and your thread moves on; a callback fires when the result lands. This uses resources far more efficiently at high throughput and is the natural fit for event-driven code β€” but it's harder to read and reason about, and error handling gets fiddlier.

The honest rule of thumb: reach for synchronous when one call's result is needed right now to continue (most user-facing request handlers); reach for asynchronous when you're firing many calls in parallel, fanning out, or can't afford to park threads. And when a call is truly fire-and-forget β€” you don't need the answer at all β€” that's a hint you might not want RPC at all, but messaging (a queue). We draw that line sharply in Part 3.


6. RPC vs. REST: the contrast in one breath

You'll be asked this constantly, so let's frame it cleanly. The deepest difference is what you're naming when you make a call.

  • REST is resource-oriented. You name a thing (/users/42) and act on it with a small, fixed set of verbs (GET, POST, PUT, DELETE). It speaks human-readable JSON over HTTP/1.1, it's cacheable, browser-friendly, and universally understood. That openness is its superpower for public APIs and third-party integrations.
  • RPC is action-oriented. You name a verb β€” getUser, chargeCard, transcodeVideo β€” and call it like a method. It speaks compact binary over (usually) HTTP/2, enforces a typed contract, and supports streaming. That tight, fast, schema-checked style is its superpower for internal service-to-service traffic.
REST RPC
You name a... resource (noun) procedure (verb)
Payload text JSON (verbose) binary (compact)
Contract informal / OpenAPI enforced IDL, generated stubs
Typical transport HTTP/1.1 HTTP/2
Caching easy (HTTP semantics) not built in
Best at public APIs, browsers, third parties internal microservices, low latency

The senior framing isn't "RPC beats REST." It's REST at the edge, RPC on the inside. Expose a REST (or GraphQL) API to the outside world for reach and flexibility; use RPC between your own services for speed and safety. Most large systems run both, and the API gateway is where one becomes the other. We'll go deeper on this β€” and add messaging and WebSockets to the comparison β€” in Part 3.


Key takeaways

  1. RPC makes a remote function call look local. A generated stub marshals your call into bytes, ships them over a transport to a server skeleton that unmarshals and runs the real function β€” and the result travels back the same way.
  2. The contract lives in an IDL (Protobuf, Thrift, Avro). Write it once; generate type-safe, cross-language stubs from it. This is what makes RPC fast and safe.
  3. The local-call illusion is a leaky abstraction. The network is not free and not reliable β€” the Fallacies of Distributed Computing are all false, and partial failure means a timeout tells you "I don't know if it ran," not "it failed."
  4. RPC comes in synchronous (block and wait, simple) and asynchronous (Future/Promise, efficient at scale) flavors; choose by whether you need the answer to proceed.
  5. REST names resources for the public edge; RPC names procedures for the internal core. The grown-up architecture uses both.

Next, Part 2 cracks the box open: serialization formats and schema evolution, the transports (TCP, HTTP/2, QUIC) and why HTTP/2 multiplexing matters, plus service discovery, load balancing, and the resilience middleware that makes RPC survivable in production.

Sign in to MAANG.io

Use your Google or Microsoft account β€” no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.