Skip to content

Remote Procedure Invocation

The Order service needs the buyer’s shipping address, which lives in the Customer service. The most natural thing in the world is to ask for it and wait for the answer, exactly as the old monolith called a getAddress method. The caller wants a single, immediate response so it can keep building the order.

You need one service to invoke an operation on another and receive a result, with as little ceremony as possible. The team already thinks in terms of methods that take arguments and return values, and the operation is genuinely a question that needs an answer before the caller can continue. How do you make a network call feel like the function call it replaced, while still being explicit about the things a function call never had to worry about — serialization, timeouts, and a contract that both sides agree on?

Remote Procedure Invocation (RPI) is the synchronous, one-to-one style. The client sends a request to a single service instance and blocks until the response arrives. A proxy or client library hides the mechanics: it serializes the arguments, sends them over the wire, waits, and deserializes the result back into an ordinary return value.

Two transports dominate:

  • REST over HTTP — resources addressed by URL, operations expressed with HTTP verbs, payloads usually in JSON. Human-readable, ubiquitous, easy to test with a browser or curl.
  • gRPC — a contract defined in a .proto file, binary Protobuf payloads over HTTP/2, with generated client and server stubs. More compact and faster, with a strict, versioned schema and first-class streaming.

Whatever the transport, the interaction is the same shape: one request, one response, the caller waiting in between.

sequenceDiagram
  participant C as Client (Order Service)
  participant S as Customer Service
  C->>S: GET /customers/42/address
  activate S
  S->>S: load customer 42
  S-->>C: 200 OK { address }
  deactivate S
  Note over C: caller was blocked until the response arrived
Remote Procedure Invocation — one request, one response, the caller blocked in between

A typed client for fetching a customer’s shipping address. The client wraps the transport so the caller writes something close to an ordinary method call, but a timeout makes the network’s existence explicit. Each example is self-contained and idiomatic to its language.

type Address = { line1: string; city: string; postalCode: string };
class CustomerClient {
constructor(private baseUrl: string) {}
async getAddress(customerId: string): Promise<Address> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2000);
try {
const res = await fetch(`${this.baseUrl}/customers/${customerId}/address`, {
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`customer service returned ${res.status}`);
}
return (await res.json()) as Address;
} finally {
clearTimeout(timer);
}
}
}
const customers = new CustomerClient('http://customer-service');
const address = await customers.getAddress('42');

What you gain:

  • Simplicity. The model is the one every developer already knows: call, wait, get a result. There is no broker to operate and no intermediate state to track.
  • A clear contract. Especially with gRPC, the request and response types are defined once and enforced for both sides, so a mismatch is caught at build time.

What it costs you:

  • Temporal coupling. Because the caller blocks, both services must be available at the same instant. If the callee is down or deploying, the caller’s request fails right now — there is no buffer in between.
  • Cascading failures. A slow callee makes the caller slow too; the caller’s threads or connections pile up waiting, and the slowness propagates upstream until an entire chain of services is stuck. A single struggling service can take down everything that depends on it.
  • Reduced availability. The overall availability of a synchronous call chain is the product of each link’s availability, so the more hops a request makes, the more fragile it becomes.

Because of these failure modes, never make a raw synchronous call without protecting it. Wrap it with timeouts, retries, and a Circuit Breaker so a failing dependency fails fast instead of dragging the caller down with it. Where the interaction does not truly need an immediate answer, prefer Messaging instead.

  • Messaging — the asynchronous alternative that removes temporal coupling.
  • Service Discovery — how the client finds the address it sends the request to.
  • API Gateway — often the front door through which external RPI calls arrive.
What kind of interaction is Remote Procedure Invocation?
What is temporal coupling in the context of RPI?
Why can a single slow service cause a cascading failure with RPI?
Which pattern should wrap a synchronous call to prevent it from dragging the caller down?