API Design Patterns — Staff Interview Quick Reference
The 60-Second Version
- REST is resource-oriented, stateless, and built on HTTP verbs. Best for external APIs and simple CRUD where broad tooling support matters.
- gRPC uses protobuf (binary), supports streaming, and enforces strong typing. Best for internal service-to-service where performance and contracts matter. Not browser-friendly without a proxy layer.
- GraphQL exposes a single endpoint with client-driven queries. Best when diverse frontends need different slices of the same data. Dangerous when it devolves into a query engine over your database.
- API style is an organizational boundary decision, not a technology preference. External = REST. Internal = gRPC. GraphQL = product teams with heterogeneous frontend needs.
- Every versioning strategy is a deprecation commitment. URL path versioning (
/v1/) is simple but forces migration. Header versioning is cleaner but harder to discover. - Pagination choice has correctness implications: offset-based skips rows on insert, cursor-based can't jump to page N, keyset scales for large datasets.
- Idempotency is a requirement, not a nice-to-have. POST is not idempotent. Client-generated idempotency keys are the only reliable way to prevent duplicate mutations.
What Staff Engineers Say (That Seniors Don't)
| Concept | Senior Response | Staff Response |
|---|---|---|
| Protocol choice | "gRPC is faster than REST" | "REST at the org boundary for discoverability, gRPC internally for contract enforcement and performance" |
| Versioning | "We use /v2/ for the new API" | "Every version is a support surface. We version at the field level and only cut a new path version for breaking contract changes" |
| Pagination | "We use limit/offset" | "Offset pagination is wrong for any dataset with concurrent writes. Cursor-based for feeds, keyset for sorted tables" |
| Idempotency | "We deduplicate on the server" | "Clients generate idempotency keys. The server stores them with a TTL. Without this, every retry is a potential double-write" |
| GraphQL adoption | "GraphQL lets clients get exactly what they need" | "GraphQL shifts query complexity to the server. Without depth limits and query cost analysis, a single client can bring down your data layer" |
The Numbers That Matter
- Payload size: Keep REST responses under 1 MB; paginate anything larger. gRPC default max message size is 4 MB.
- Timeout defaults: Service-to-service calls: 500ms-1s. External API gateway: 5-10s. Never leave timeouts unset.
- Pagination limits: Default page size 20-50 items. Hard cap at 100-200. Cursor tokens should expire after 24h for consistency.
- Idempotency key TTL: 24-48 hours covers retry windows without unbounded storage growth.
- Rate limiting: Per-client defaults of 100-1000 req/s depending on tier. Always return
429withRetry-Afterheader.
Common Interview Traps
- Choosing gRPC "because it's faster" without discussing tradeoffs. Staff answer: it's faster and you lose browser support, human-readable debugging, and broad proxy compatibility.
- Ignoring idempotency on write endpoints. If you design a POST endpoint without an idempotency strategy, you've designed a bug.
- Treating pagination as a UX concern. It's a correctness and scalability concern. Offset pagination breaks under concurrent writes.
- Defaulting to GraphQL without discussing operational cost. Unbounded query depth, N+1 resolution, and cache invalidation complexity are real production risks.
API Style Decision Matrix
Rendering diagram...
Pagination Deep Dive
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Offset/Limit | ?offset=20&limit=10 | Simple, jump to any page | Skips/duplicates on concurrent inserts; O(N) on large offsets |
| Cursor-based | ?after=eyJ0... | Stable under inserts; efficient for append logs | Can't jump to page N; opaque tokens |
| Keyset | ?created_after=2024-01&id_gt=500 | Efficient at any depth; composable filters | Requires sortable unique column; harder to implement |
| Page token (Google-style) | Server-issued opaque token | Server controls; can change implementation | Token expiry needs handling; stateful or encoded |
Staff rule: If the dataset has concurrent writes (most production systems), offset pagination is wrong. Default to cursor-based for feeds, keyset for sorted tables.
Practice Prompt
Staff-Caliber Answer ShapeExpand
- REST for the external API. Third-party integrations expect REST — it's discoverable, well-documented, and proxy-friendly. Use resource-oriented design:
GET /products,POST /products,GET /products/{id}. - GraphQL as an optional BFF layer for first-party clients. Web and iOS apps need different data shapes from the same endpoints. A GraphQL layer over the REST backend lets each client request exactly what it needs without building per-client endpoints.
- gRPC for internal services. Product catalog service, search service, and pricing service communicate via gRPC for type safety and performance.
- Versioning: URL path versioning for the public REST API (
/v1/products). Field-level deprecation for GraphQL (mark fields@deprecated). Protobuf versioning for gRPC (backward-compatible field additions). - Idempotency:
POST /ordersrequires a client-generatedIdempotency-Keyheader. Server stores keys for 48 hours. Retries within that window return the cached response.
The Staff move: Use different API styles at different boundaries. Don't force one style everywhere.
Additional Traps
- Exposing internal IDs in external APIs. Auto-increment IDs leak information (total count, creation order). Use UUIDs or hashids for external-facing resources.
- Missing error contract. Returning bare HTTP status codes without structured error bodies makes client error handling impossible. Define an error schema:
{ "code": "INVENTORY_EXHAUSTED", "message": "...", "details": {...} }. - Tight coupling via shared types. Services sharing protobuf definitions is fine; services importing each other's domain models is a coupling trap. Define API contracts at the boundary, not the implementation.
- No rate limiting on write endpoints. Every mutating endpoint needs per-client rate limiting. Without it, one misbehaving client can overwhelm your write path.