263: Service Communication: REST, RPC/gRPC, GraphQL, WebSockets, SSE, and Polling
Learning outcomes
By the end of this lesson, you should be able to:
- explain and apply REST/HTTP APIs in a realistic implementation;
- explain and apply RPC and gRPC in a realistic implementation;
- explain and apply GraphQL in a realistic implementation;
- explain and apply WebSockets in a realistic implementation;
- explain and apply SSE in a realistic implementation.
Prerequisites and retrieval
This lesson assumes the 01–06 foundation and the earlier lessons in this module. Before you start, retrieve one concrete example from a previous project where this same concern appeared. Perhaps you chose an HTTP endpoint for a browser client, streamed job progress, or had to decide how an internal service should call another service. The point is not to memorize protocol names. It is to make a defensible choice for a large-scale distributed service, where requirements, traffic, failure modes, cost, and operational constraints all need to be explicit.
Terminology
- REST/HTTP APIs: Resource- or action-oriented HTTP APIs work well across languages and client types. They are interoperable, cacheable, and easy to inspect with browser and network tooling, which makes them a strong fit for browser and public clients. The API still needs disciplined contracts and versioning; using HTTP does not remove those responsibilities.
- RPC and gRPC: RPC presents a remote operation as a service method. gRPC adds protobuf contracts, generated client and server code, streaming, and efficient binary framing. Those properties are particularly useful for controlled internal clients, where both sides can adopt the contract and tooling.
- GraphQL: A GraphQL client requests the fields it needs from a schema-shaped graph. This can reduce some over-fetching and under-fetching, but the server must handle resolver batching, authorization at the field or object boundary, query-cost limits, and a more complicated caching model.
- WebSockets: WebSockets provide a persistent, bidirectional connection. That shape fits chat, collaboration, and control flows, but it also makes connection scaling, heartbeats, reconnection, authentication refresh, and distributed fan-out part of the design.
- SSE: Server-sent events provide a simpler server-to-browser stream over HTTP. They fit notifications, progress, and feed updates when the client-to-server part can remain ordinary HTTP requests. The direction is intentionally one-way from server to client.
- Polling and long polling: Regular polling is robust and straightforward to operate when moderate freshness is acceptable. Long polling avoids some empty responses by holding a request until an update or timeout, but it still consumes connection and request-lifecycle resources.
Mental model
Treat Service Communication: REST, RPC/gRPC, GraphQL, WebSockets, SSE, and Polling as a design problem with observable inputs, outputs, invariants, and failure modes. The interaction shape should drive the communication style: consider directionality, compatibility, latency, client diversity, and the operational tools available to the team. There is no universally correct “microservices protocol.” A strong implementation makes its assumptions visible, narrows uncertainty at system boundaries, and leaves enough evidence—tests, types, constraints, metrics, or diagrams—to show why the design is safe.
A useful sequence for both interviews and production work is:
requirement -> constraints -> model -> implementation -> failure analysis -> verification
Do not go straight from a requirement to a library call. First write down what must remain true. Then choose the mechanism that makes those properties easier to enforce and observe. For example, “the browser can receive progress updates” is not enough to choose WebSockets: you also need to ask whether the browser must send messages on the same channel, how fresh the updates must be, and what happens when the connection drops.
Deep dive
1. REST/HTTP APIs
When clients vary widely or the API must be public, ordinary HTTP is often the easiest boundary for people and tools to understand. Resource/action HTTP APIs are interoperable, cacheable, and debuggable, and they fit browser and public clients well. The useful distinction is between choosing HTTP for those properties and enforcing strict REST purity for its own sake. In practice, contract and version discipline usually matter more than whether every endpoint satisfies an idealized REST definition.
Decision rule: Use REST/HTTP APIs deliberately when they make the contract or its invariants easier to prove. If the choice only reduces typing while hiding an assumption about validation, retries, authorization, or consistency, prefer the more explicit design.
2. RPC and gRPC
RPC is a natural model when one service needs to invoke a well-defined operation on another service. gRPC strengthens that boundary with protobuf schemas, generated code, streaming support, and efficient binary framing. That can make internal service-to-service communication more consistent, but it does not make a remote call local: latency, timeouts, partial failure, retries, and compatibility still need to be designed.
Decision rule: Use RPC and gRPC deliberately when they make the contract or its invariants easier to prove. If they only reduce typing while hiding an assumption about failure handling or deployment compatibility, prefer the more explicit design.
3. GraphQL
GraphQL is useful when different clients need different projections of the same connected data. A client can request a graph-shaped selection from a schema instead of depending on a separate endpoint for every combination of fields. That flexibility can reduce some over-fetching and under-fetching. It also moves complexity into the server: resolvers need batching, authorization must be enforced consistently, query cost may need limits, and cache behavior is less automatic than with many conventional HTTP responses.
Decision rule: Use GraphQL deliberately when it makes the client data contract or its invariants easier to prove. If it only reduces endpoint count while hiding resolver cost, authorization, or cache complexity, prefer the more explicit design.
4. WebSockets
WebSockets fit interactions in which both sides need to send messages over a long-lived connection, such as chat, collaboration, or control. The connection itself becomes a resource that must be managed. At scale, plan for connection distribution, heartbeats, reconnection, authentication refresh, and fan-out across instances. A connected client can disappear without a clean application-level shutdown, so the server must not treat the connection as permanent or assume that every message is delivered.
Decision rule: Use WebSockets deliberately when bidirectional persistence makes the contract or its invariants easier to prove. If the client only needs server-to-browser updates, or if the connection lifecycle hides more operational risk than it removes, consider SSE or polling instead.
5. SSE
SSE is a good fit for server-to-browser notifications, progress updates, and feeds when client commands can use ordinary HTTP. It keeps the communication direction simple while still allowing the server to stream events over an HTTP connection. The simpler protocol does not eliminate lifecycle concerns: clients can reconnect, events can be delayed or missed, and the system must decide whether an event is merely a hint to refresh or a durable item that requires replay.
Decision rule: Use SSE deliberately when one-way server-to-client streaming makes the contract or its invariants easier to prove. If the client also needs frequent messages on the same persistent channel, or if the required delivery semantics are not supported by the design, evaluate WebSockets or a durable message workflow.
6. Polling and long polling
Polling is often the right answer when the freshness requirement is moderate and operational simplicity matters. The client asks at an interval and can recover naturally after a failed request. The trade-off is predictable: short intervals increase request and server load, while long intervals increase staleness. Long polling reduces empty responses by keeping a request open until an update is available or a timeout occurs, but it still uses connection and request-lifecycle resources and still needs timeout and retry behavior.
Decision rule: Use polling or long polling deliberately when the freshness, load, and recovery trade-offs are acceptable and easy to prove. If the requirement needs low-latency continuous updates, or if the polling load grows with too many clients, consider SSE or WebSockets.
Worked example
Consider a large-scale distributed service whose requirements, traffic, failure modes, cost, and operational constraints must be explicit. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which of the communication concepts above owns each failure mode. The important move is separation: parsing and validation belong at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Combining these concerns can make a happy-path demo look shorter, but it makes edge cases and failures much harder to reason about.
Client
|
DNS -> CDN / Edge
|
Load Balancer -> API instances -> Cache
| |
+------> Primary datastore
|
+------> Queue / Stream -> Workers
Walk through at least four cases: the normal path; an empty or missing value; a duplicate, retry, or concurrent path where relevant; and a dependency failure. For each case, state which layer detects the problem, which communication boundary carries the result, and what the caller observes. For a stream, include what happens after disconnect and reconnect. This is the level of explanation expected in a senior code review or technical interview: not merely naming a protocol, but showing where the contract is enforced and how failure is made visible.
Production perspective
Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality traffic. Persistent connections add connection counts, heartbeat traffic, and reconnect storms to that list. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after evidence identifies a bottleneck or risk.
When the topic involves an external dependency, define timeout and cancellation behavior. A caller should not wait forever because a downstream service or stream stopped responding. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and all network input is untrusted; a protocol choice never substitutes for server-side authorization and validation.
Guided lab
For chat, payment processing, a public CRUD API, internal recommendation calls, and job progress, choose a communication style for each. Justify each choice in terms of compatibility, directionality, latency, and failure handling. There may be more than one defensible answer, but the justification should expose the assumptions that make the choice appropriate.
Complete the lab with this discipline:
- Write the requirement and two non-requirements.
- List input, output, and error contracts before implementation.
- Implement the smallest correct vertical slice.
- Add at least one invalid-input test and one edge-case test.
- Instrument or inspect the behavior instead of guessing.
- Refactor one hidden assumption into an explicit type, constraint, function, or configuration value.
- Explain one alternative design and why you did not choose it.
- Record a short “what would break at 10× scale?” note.
Edge cases and failure modes
- REST/HTTP APIs: Test absent and malformed input, duplicate requests, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Also decide what a client should do with timeouts and retryable responses; a retry can repeat a side effect unless the operation is designed to be idempotent.
- RPC and gRPC: Test absent and malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Include deadlines, cancellation, incompatible schema changes, and the possibility that the server completed work even though the client lost the response.
- GraphQL: Test absent and malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Include authorization on nested data, expensive queries, resolver batching, and partial errors in a response.
- WebSockets: Test absent and malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Include dropped connections, reconnects, missed messages, heartbeat timeouts, token refresh, and fan-out across multiple instances.
- SSE: Test absent and malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Include reconnect behavior, event identity and replay decisions, proxy or idle timeouts, and slow clients.
Common mistakes and debugging
- Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
- Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary”
anyvalues. - Testing only the happy path and discovering the real contract only during integration.
- Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
- Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.
- Treating a remote call or persistent connection as if it had local-call reliability and delivery semantics.
For debugging, reproduce the smallest failing case and inspect the actual value, wire request, response, event stream, or execution plan. Trace the boundary where the invariant first becomes false. Then fix the layer that owns the rule instead of adding a downstream patch. Check the client and Network panel for browser-facing behavior, the route and server logs for request handling, the database or query plan for persistence behavior, and deployment or proxy configuration for timeouts and connection limits.
Interview questions
- What problem do REST/HTTP APIs solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do RPC and gRPC solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does GraphQL solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do WebSockets solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does SSE solve, and what trade-off or failure mode would make you choose a different approach?
Checkpoint
Without notes, explain Service Communication: REST, RPC/gRPC, GraphQL, WebSockets, SSE, and Polling to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be able to explain not only why the selected protocol fits, but also what the caller observes when the dependency or connection fails.
Mastery checklist
- I can define the core terms precisely.
- I can choose a design from requirements instead of from habit.
- I can implement and test the normal path and edge cases.
- I can explain the runtime, storage, or complexity cost.
- I can identify which layer owns validation, errors, and recovery.
- I can compare at least two reasonable alternatives.
- I can explain how the design changes at larger scale or stricter reliability.
