CASE STUDY

REST API Abstraction Layer (Internal Service-Client SDK)

3 min read·482 words·Intermediate

Asked at

1 candidate report in Apr 2026

How to use this case study

SDE-2 / Mid

Explain the boilerplate problem (building requests, auth, retries, JSON parsing, errors) and a typed client that hides it.

SDE-3 / Senior

Design a middleware pipeline (auth, retries with backoff, timeouts, circuit breaker, tracing, metrics) and a consistent error model.

Staff / Principal

Discuss generating clients from OpenAPI specs, versioning and backward compatibility, configuration per service, and rollout across many teams.


0) Problem Restatement

Snowflake asked: a company runs many application servers that call internal REST services. Today, every caller hand-writes the same boilerplate for each call: build the URL and HTTP request, add auth headers, serialize JSON, set timeouts, retry on failures, parse responses, map errors, and log. This leads to copy-paste bugs and inconsistent behavior (some callers never retry, others retry forever). Design a reusable service-client SDK that removes the boilerplate and makes calls consistent.


1) Goals

  • A caller writes orders = client.orders.list(user_id=42) and gets typed results.
  • Consistent timeouts, retries, auth, tracing, metrics and error handling everywhere.
  • Easy to add new services and endpoints.
  • Configurable per service (timeouts, base URL), and safe defaults.


2) Design

Architecture Diagram

flowchart LR
    APP["Application code"] --> TC["Typed client - orders.list(), users.get()"]
    TC --> PIPE["Middleware pipeline"]
    PIPE --> AUTH["Auth - service token"]
    AUTH --> TR["Tracing + metrics"]
    TR --> RET["Retry + backoff (idempotent only)"]
    RET --> CB["Circuit breaker + timeout"]
    CB --> HTTP["HTTP transport - pooled connections"]
    HTTP --> SVC["Internal REST service"]
    SVC --> ERR["Response parsing + error mapping"]
    ERR --> TC

2.1 Typed clients (generated)

  • Each service publishes an OpenAPI spec. A code generator produces typed client classes and models (in Java, Python, Go...), so method names, parameters and response types come from the spec. When the spec changes, regenerate.
  • Generated code handles paths, query and body serialization, and JSON parsing into typed objects.

2.2 Middleware pipeline (hand-written once, shared)

Like interceptors or filters, each concern is one small, testable piece:

  • Auth: attach a short-lived service token (fetched and refreshed automatically).
  • Tracing: propagate trace IDs, and create a span per call.
  • Metrics: latency, status codes, retries per service and endpoint.
  • Retries: only for idempotent methods (GET, PUT, DELETE) or calls with an idempotency key. Exponential backoff with jitter, a max attempts count, and a retry budget so outages aren't amplified.
  • Timeouts: per-call defaults from config, and deadline propagation from the incoming request.
  • Circuit breaker: fail fast when a service is unhealthy.
  • Logging: structured, with secrets redacted.

2.3 Consistent error model

Map HTTP and network errors into a small set of exception types: NotFound, Unauthorized, RateLimited(retry_after), ValidationError(details), ServiceUnavailable, Timeout. Callers handle meaning, not raw status codes.


3) Example

client = ServiceClient.for_service("orders")          # config: base_url, timeouts from registry
try:
    page = client.orders.list(user_id=42, limit=20)   # typed request, typed response
    for o in page.items:
        print(o.id, o.total)
except RateLimited as e:
    schedule_retry(after=e.retry_after)
except NotFound:
    ...

4) Rollout and Evolution

  • Config registry: base URLs, timeouts and retry policies per service, changeable without code changes.
  • Versioning: services keep backward-compatible changes within /v1. The SDK is semantically versioned, and deprecated methods log warnings.
  • Adoption: start with the most-used services, provide migration guides, and lint rules that flag raw HTTP calls to internal services.
  • Testing: generated clients have mock servers (from the spec) for callers' unit tests.


5) Wrap-Up

Generate typed clients from each service's OpenAPI spec, and run every call through one shared middleware pipeline: auth tokens, tracing and metrics, retries only for idempotent calls with backoff and a budget, timeouts with deadline propagation, circuit breakers and redacted logging. Map responses into a consistent error model, drive per-service settings from a config registry, and roll it out with versioning, mocks and lint rules that discourage hand-written HTTP calls.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →