API and Interface Design for Distributed Services Questions
Designing the contracts between services and clients: REST, gRPC, and GraphQL tradeoffs, versioning and backward compatibility, pagination, rate limiting, and idempotent endpoints. Covers request/response modeling, error contracts, and API gateway responsibilities. Focuses on the interface layer that ties distributed components together, not internal data schemas.
You need to choose the API communication protocol for internal microservices: REST/JSON, gRPC, or GraphQL. Propose a weighted scoring approach: define the evaluation criteria you would use, assign relative weights, and justify how you would score each option for a medium-scale microservices environment.
Sample Answer
Direct answer
Build a small rubric of weighted criteria that map to what actually matters for this environment (latency behavior, streaming support, developer ergonomics, language and tooling coverage, observability, and operational cost), score each candidate protocol from 1 to 5 against each criterion, multiply by weight, and let the arithmetic surface the leading option rather than gut feel, then sanity-check the result with a short proof of concept, a small prototype built specifically to validate the winning assumption, before treating it as final.
Criteria and weights
Weights should sum to 100% and reflect what a medium-scale internal microservices environment actually cares about, not a generic checklist:
| Criterion | Weight | Rationale |
|---|---|---|
| Latency and performance | 20% | Core cost for internal request-response traffic |
| Streaming and real-time capability | 18% | Needed wherever event or continuous-transfer use cases exist |
| Developer ergonomics | 16% | Directly affects onboarding speed and day-to-day productivity |
| Language and tooling support | 14% | Ecosystem maturity across a polyglot stack |
| Observability and debuggability | 16% | Determines how fast incidents get diagnosed in production |
| Operational cost and complexity | 16% | Ongoing maintenance and infrastructure burden |
Scoring each option
| Criterion (weight) | REST/JSON | gRPC | GraphQL |
|---|---|---|---|
| Latency (20%) | 3, plain HTTP/1.1 plus JSON parsing overhead | 5, HTTP/2 plus binary protocol buffers | 3, similar transport cost to REST plus resolver overhead |
| Streaming (18%) | 2, no native support, needs a bolt-on | 5, streaming is a first-class protocol feature | 3, subscriptions exist but are heavier to operate |
| Ergonomics (16%) | 4, simple and familiar | 3, steeper learning curve for schema and generated clients | 4, strong client-side flexibility |
| Tooling (14%) | 5, universal client and proxy support | 4, strong but less universal than plain HTTP | 4, mature but narrower ecosystem |
| Observability (16%) | 4, works with mature HTTP-native tooling | 3, needs extra config for some proxies and tracing setups | 3, resolver-level tracing needs deliberate setup |
| Operational cost (16%) | 4, lowest day-to-day friction | 3, more moving parts (codegen, proto registry) | 2, added server complexity and the N+1 resolver risk (a naive nested-field resolver can issue a separate database call per item instead of one batched query) |
Computing the result
The weighted total for each option is score=∑iwi×si, computed directly from the table above:
REST/JSON: 3(0.20)+2(0.18)+4(0.16)+5(0.14)+4(0.16)+4(0.16)=0.60+0.36+0.64+0.70+0.64+0.64=3.58
gRPC: 5(0.20)+5(0.18)+3(0.16)+4(0.14)+3(0.16)+3(0.16)=1.00+0.90+0.48+0.56+0.48+0.48=3.90
GraphQL: 3(0.20)+3(0.18)+4(0.16)+4(0.14)+3(0.16)+2(0.16)=0.60+0.54+0.64+0.56+0.48+0.32=3.14
| Protocol | Weighted total |
|---|---|
| REST/JSON | 3.58 |
| gRPC | 3.90 |
| GraphQL | 3.14 |
Under these weights and scores, gRPC wins for an internal, medium-scale, latency- and streaming-sensitive environment; REST is a close second, well ahead of GraphQL, which is dragged down here specifically by operational complexity and the streaming gap rather than any single criterion.
Trade-offs and pitfalls
A weighted total gives a defensible number, but the weights themselves encode a value judgment the team needs to agree on before the score means anything. If two totals are close (REST at 3.58 versus gRPC at 3.90 here), re-run the same rubric with a couple of alternate weightings, for example dropping streaming's weight and redistributing it to operational cost, to see whether the ranking is sensitive to your specific weighting choices or genuinely robust. A ranking that flips under a plausible alternate weighting is a signal to validate with a real proof of concept rather than trust the score alone; a ranking that holds up across several reasonable weightings is much stronger evidence. Never fill in the 1-to-5 scores from a protocol's general reputation without checking that the specific criterion actually applies to your workload and your team's current skill set.
Design a REST API for listing and creating 'products' that supports pagination, filtering, sorting, and versioning. Specify the request/response shapes, your pagination strategy, your versioning approach, and how you would roll out a breaking change to both internal and external clients without a hard cutover.
Sample Answer
Direct answer
Model the collection with a resource-oriented URL (GET/POST /v1/products), cursor pagination for listing, structured filter and sort query parameters, and a URI-path major version (/v1/). To roll out a breaking change without a hard cutover, let the old and new response shapes run side by side under different version paths, keep the new field additive first, and retire the old version only once usage telemetry shows it is safe, not on a calendar date alone.
Framework
Endpoints and shapes
| Method & path | Purpose |
|---|---|
GET /v1/products?limit=20&after=<cursor>&sort=price:asc&status=active&category=tools | List products, paginated, filtered, sorted |
POST /v1/products | Create a product |
GET /v1/products/{id} | Fetch one product |
PATCH /v1/products/{id} | Partial update |
GET /v1/products?limit=2&sort=price:asc
200 OK
{
"data": [
{ "id": "p1", "name": "Widget", "price": 9.99, "status": "active" },
{ "id": "p2", "name": "Gadget", "price": 14.50, "status": "active" }
],
"page": { "limit": 2, "next_cursor": "<opaque-cursor-for-p2>", "has_more": true }
}
POST /v1/products
{ "name": "New Widget", "price": 19.99, "category": "tools" }
201 Created
Location: /v1/products/p3
{ "id": "p3", "name": "New Widget", "price": 19.99, "category": "tools", "status": "active" }
Pagination strategy: cursor-based, for the same reason as any list that can grow and be written to concurrently: stable under inserts, roughly constant query cost at depth, at the cost of not supporting a direct "jump to page 43." Offer limit and an opaque after cursor rather than page/offset.
Filtering and sorting: plain query parameters for common fields (status=active, category=tools), and a single sort=field:direction parameter, comma-separated for multiple fields, for example sort=price:asc,name:desc. The filterable field vocabulary is documented per field in the OpenAPI spec (a machine-readable API description) so clients know exactly what is filterable rather than guessing.
Versioning: the major version lives in the URL path (/v1/), because it is simple to route at a gateway or load balancer without inspecting headers, and it is trivially cacheable per version. Additive changes, new optional fields, new optional query parameters, ship inside v1 without a version bump; anything that removes a field, changes a field's type, or changes default sort or filter behavior goes into v2.
Rolling out a breaking change without a hard cutover: suppose v2 needs to rename price to unit_price and change it from a decimal amount to an integer number of cents.
- Ship
v2alongsidev1on the same deployment.v2's handler reads and writes the same underlying data asv1, so there is exactly one source of truth behind two response shapes. - Mark
v1'spricefield deprecated via a response header and a documented sunset date, while it keeps working exactly as before. Nothing breaks yet. - Instrument both versions: tag every request with which version served it, and track the fraction of traffic still hitting
v1. - Migrate internal clients first, since you can coordinate with them directly, then notify external and partner clients with a fixed migration window, for example 90 days, pointing at the
v2docs and a short code sample. - Remove
v1only once telemetry shows its remaining traffic has dropped to a level you have explicitly decided is safe to force-migrate, for example only a handful of clients you can contact individually, not on the calendar date alone. If a significant client is still onv1at the deadline, extend the window rather than break them, and treat that as a signal the migration tooling or communication needs work.
Worked example
Concretely, at the moment v2 ships, v1 carries 100% of traffic. After internal clients migrate in the first month, v1's share drops to 70%. Announcing the 90-day window to external partners brings it down further; by day 60, v1 is at 8%, all from three named partner integrations already contacted directly about their remaining migration steps. That 8%-and-named state, not the passage of 60 days on its own, is what tells you it is close to safe to set a hard removal date, once those three integrations confirm.
Trade-offs and pitfalls
- Renaming
pricetounit_priceand changing its unit, dollars to cents, in the same release conflates two changes into one migration. A client that only cared about the rename still has to handle the unit change, which raises the odds the migration is done wrong. Prefer landing one breaking change at a time when volume allows it. - A common pitfall is announcing deprecation only in documentation and not in the response itself. Clients that never read a changelog will not notice until the sunset date arrives, so a machine-readable response header, not prose, is what actually drives safe removal.
- Cursor pagination combined with
sortneeds care: the opaque cursor usually encodes the sort key's value, so changingsortmid-pagination, fetching page 1 by price then asking for page 2 by name, should be rejected or restarted from page 1, since a cursor from one sort order is meaningless under a different one.
That is every published API and Interface Design for Distributed Services question for Engineering Manager so far. Browse the other topics in this category, or practice this one interactively.