Backend / Microservices / 10_internal_sdks.md

Internal SDKs for inter-service calls

Updated 5 interview angles 4 min read source
On this page6
  1. What the SDK actually owns
  2. Generated or hand-written
  3. The coupling problem
  4. The alternative worth naming
  5. Related
  6. Interview angle

Internal SDKs for inter-service calls

When a company runs a dozen services, each one calling another over HTTP grows the same 200 lines of client code: base URL, auth, retries, timeouts, error mapping. The internal SDK is the decision to write that once and ship it as a package.

What the SDK actually owns

The failure mode without one is not “duplicated code” - it is inconsistent failure behaviour. Service A retries a 500 five times, service B not at all, service C retries a 400 forever. None of that is visible until an incident.

An internal SDK owns the policy:

Concern What the SDK fixes
Auth Token fetch, refresh, and rotation
Retries What is retryable, and the backoff
Timeouts A default nobody forgets to set
Errors HTTP status to typed exception
Tracing Context propagation headers

The calling service imports a method, not a URL.

python
# Without: every caller re-derives the contract.
r = await client.get(
    f"{settings.billing_url}/v1/invoices/{id}",
    headers={"Authorization": f"Bearer {tok}"},
    timeout=5.0,
)
r.raise_for_status()
return Invoice(**r.json())

# With: the contract is the signature.
return await billing.invoices.get(id)

Generated or hand-written

Two approaches, and the choice sets your maintenance cost.

Generated from a spec. The service publishes OpenAPI or protobuf; CI generates a typed client on every release. The contract cannot drift from the implementation because it is derived from it.

Hand-written. A human writes and versions the client. It reads better and can expose a nicer API than the wire format, but it is a second thing to update, and it will lag.

Generate the transport, hand-write the ergonomics on top. gRPC gives you this split for free - the stub is generated, and you wrap it.

python
class BillingClient:
    def __init__(self, stub: BillingStub):
        self._stub = stub          # generated

    async def get_invoice(self, id: str) -> Invoice:
        try:
            return await self._stub.GetInvoice(
                GetInvoiceRequest(id=id)
            )
        except AioRpcError as e:
            raise _map(e) from e   # hand-written

The coupling problem

This is the objection an interviewer is looking for. A shared client library is a shared dependency, and it can undo the independence microservices exist to buy.

Three specific ways it bites:

  • Lockstep upgrades. If the SDK bundles the server’s models and the server changes them, every consumer must upgrade to stay compatible. That is a distributed monolith wearing a package manifest.
  • Transitive conflicts. The SDK pins httpx>=0.27; a consumer pins httpx<0.27. Now a billing change blocks an unrelated deploy.
  • Logic leaking in. Someone adds a “convenience” method that computes business rules client-side. Now the rule has two implementations.

Keeping it thin

  • Version the SDK independently of the service, and support the last two major API versions so consumers upgrade on their own schedule.
  • Depend on as little as possible. An SDK that pulls in a web framework is a liability. Prefer a narrow HTTP client and stdlib types.
  • No business logic. Serialise, call, map errors, return. If a method makes a decision, it belongs on the server.
  • Never share the server’s ORM models. Ship a separate DTO. Sharing the model means a database migration ripples into every consumer.

The alternative worth naming

A service mesh moves retries, timeouts, mTLS and tracing into a sidecar, so the SDK shrinks to types and serialisation. That is the right split when services are polyglot: you cannot ship a Python SDK to a Go team, but Envoy does not care what language it is proxying.

Use a mesh for transport policy and an SDK for typed ergonomics. See Service Mesh.

Interview angle 5

  • “How do your services call each other?” - through a versioned internal SDK rather than raw HTTP in each caller, so auth, retries, timeouts, error mapping and trace propagation have one implementation and one behaviour. Callers get a typed method, not a URL.
  • “What is the risk of a shared client library?” - it recreates the coupling microservices were meant to remove. If the SDK bundles the server’s models, a server change forces a lockstep upgrade across every consumer, and its transitive pins can block unrelated deploys. That is a distributed monolith.
  • “How do you keep it from becoming that?” - version the SDK separately from the service, support the last two API versions, keep dependencies minimal, ship DTOs rather than the server’s ORM models, and allow no business logic in it. If a method decides something, it belongs on the server.
  • “Generate it or write it?” - generate the transport from OpenAPI or protobuf so the contract cannot drift from the implementation, then hand-write a thin ergonomic layer over it for error mapping and naming.
  • “When would you not use one?” - polyglot services, where you would need an SDK per language. Then push retries, timeouts, mTLS and tracing into a service mesh sidecar and keep the client to types and serialisation.