The SDK design decision record: why the SDK scope and language targets you chose determine your maintenance burden and your API breaking change surface

SDK decisions are made in three founding sessions that never document the consequences — the "let's build a client library" session that ships a JavaScript SDK without specifying which endpoints carry a stability guarantee or what constitutes a breaking change, the "add Python support" session that ports the JS SDK without specifying the idiomatic API conventions that govern method naming and error handling in Python, and the "document the SDK" session that publishes reference docs without specifying the deprecation notice period or the major version support window. What none of these sessions produce is the stable public API surface boundary, the breaking change classification, or the language-target idiomatic convention specification that determines whether the SDK is a trusted integration surface or a maintenance liability that breaks consumer code on every update.

A 17-person developer tools startup built an event analytics platform. In month four, three enterprise customers asked for a client library to integrate the platform into their data pipelines. The "build a JavaScript SDK" session published an npm package that wrapped the REST API's 22 core endpoints. The session lasted three hours: generate a client class, add methods for each endpoint, write a README with five usage examples, publish to npm. By month ten, 40+ customer integrations depended on the package. The dependency constraint in most customer codebases was ^1.0.0 — a semver caret that allows minor and patch updates to install automatically.

In v1.3.0, the team renamed client.events.list() to client.events.query(). The rationale was alignment with their new GraphQL API, where the equivalent operation was called events.query — consistent naming across REST and GraphQL would reduce developer confusion. The method returned identical data in both v1.2.0 and v1.3.0. The changelog entry read: "v1.3.0 — API cleanup: renamed list method for consistency with new GraphQL API." The team considered this a cosmetic change.

Thirty-four customer integrations broke on their next deployment cycle. The caret constraint meant they received v1.3.0 automatically. client.events.list is not a function errors appeared in production logs. The customer success team received 34 breakage reports over 72 hours. Three enterprise customers escalated — two drafted formal breach-of-contract notices citing service disruption. One threatened to exit their contract and migrate to a competitor. The on-call engineering team deployed a hotfix in v1.3.1 that re-added client.events.list() as a deprecated alias pointing to client.events.query(). The alias would be removed in v2.0.0 — which was now required to be planned, scoped, communicated, and migrated as a major breaking release rather than a future minor cleanup.

The engineering team investigated what had gone wrong. The SDK had no document specifying which methods were part of the stable public API. It had no policy defining what constituted a breaking change. It had no deprecation notice period. The session that built the SDK had not answered the question that determined whether renaming a method was a breaking change: does consumer code reference the method by name? It does — and any rename of a publicly documented method is a breaking change regardless of whether the data returned is identical, because the reference to the old name in consumer code fails at runtime the moment the name no longer exists. The changelog entry that described a breaking change as "API cleanup" was not malice — it was the absence of a documented definition of what "breaking" means for this SDK.

A 20-person API-first infrastructure company shipped a JavaScript SDK in month six. In month fifteen, customer requests for a Python integration drove the decision to add a Python SDK. The "port the JS SDK to Python" session assigned the task to an engineer who had not worked on the JS SDK. The approach was pragmatic: open the JS SDK source, translate each method from JavaScript to Python, publish to PyPI. The session produced a working Python package in two days.

The Python SDK shipped with camelCase method names: getDeployments(), createApiKey(), updateWebhookEndpoint(), deleteWorkspaceMember(). PEP 8, Python's style guide for public APIs, specifies that method names should be lowercase with words separated by underscores: get_deployments(), create_api_key(), update_webhook_endpoint(). The guide is not advisory — Python's static analysis tools, linters (pylint, flake8, ruff), and type checkers flag camelCase method names on public classes as violations. Enterprise customers who enforced clean linting in CI received lint failures when they imported the SDK.

Twelve Python developers opened GitHub issues in the first week pointing out the PEP 8 violations. The team triaged them as "known, will fix in v2." A community member opened a pull request that added snake_case aliases alongside the camelCase methods. The team declined it — they did not want to maintain two sets of names and wanted a clean rename in v2. Meanwhile, 200+ integrations adopted the Python SDK in its camelCase form across the eight months before v2.

V2.0.0 shipped with snake_case methods. The migration guide listed 89 method renames. Community integrations broke on upgrade. The Discord server tracked 160 distinct integration breakage reports over two weeks. Several integrations were maintained by engineers who had left their companies; the integration owners had no capacity to upgrade and pinned to v1.x.x indefinitely, meaning the team now had two major versions to support. The founding session that produced the Python SDK had not documented the idiomatic API conventions that govern Python public APIs — not because the engineer was unaware of PEP 8, but because the session's goal was "working Python client" and the idiomatic convention specification was not in scope.

Structural properties set by the SDK design decision

Three structural properties are determined when a team decides to publish an SDK. None appear explicitly in the founding session that wraps the first set of API endpoints — they are the operational consequences of choices made under the pressure of responding to customer requests for a client library before a competitor ships one.

Property 1: The SDK scope and stable public API surface boundary. An SDK does not and should not wrap every endpoint in the API. Some endpoints are intended for internal use — administrative operations that require elevated credentials, internal observability queries, webhook replay triggers that are rate-limited to prevent abuse, diagnostic endpoints that return internal system state. Some endpoints are experimental — new features in early access that may be renamed, restructured, or removed without notice as the team learns from usage. Some operations are accessible via the raw HTTP API but do not have stable enough semantics to warrant a versioned SDK method.

The stable public SDK surface is the explicit enumeration of which resources and operations are included in the SDK with a stability guarantee (a method exists for this operation and changes to its signature will follow the breaking change policy), which are accessible via raw HTTP API but not wrapped in a versioned SDK method (consumer code can call these via the SDK's underlying HTTP client, but changes to these endpoints do not trigger the versioning policy), and which are internal-only and not publicly documented (consumer code must not call these and may receive no notice before they change or disappear). This boundary determines the breaking change surface — any change to a method in the stable public surface is potentially breaking and must be evaluated against the breaking change classification. Any change to an endpoint that is explicitly outside the stable surface does not trigger the SDK versioning policy.

The API versioning decision record documents the versioning contract for the REST API; the SDK versioning policy is a separate document that governs the SDK surface specifically, not the API surface. The two interact: when the API deprecates an endpoint, the SDK must deprecate the corresponding method on the same or earlier timeline; when the API adds a new endpoint, the SDK team must decide whether to add the new endpoint to the stable SDK surface or make it available via the raw HTTP client only. These decisions must be made in the SDK ADR rather than ad-hoc in the pull request that adds the new method, because ad-hoc decisions accumulate into an undocumented surface boundary that neither the SDK team nor the consumers understand.

The surface boundary also determines the SDK's dependency on the API's internal structure. A method that wraps a single API endpoint is straightforward to maintain — changes to the endpoint propagate to the method. A method that combines multiple API calls (a convenience method that fetches a resource and its related objects in a single SDK call, or a pagination helper that automatically fetches all pages of a list endpoint) has a more complex dependency: it must be updated when either API endpoint changes, and it may produce inconsistent results if one endpoint updates faster than the other. Convenience methods that span multiple API calls must be listed separately in the SDK scope document with an explicit note on which API endpoints they depend on, because their maintenance cost is higher than single-endpoint wrappers and their stability guarantee is harder to uphold.

Property 2: The language targets and idiomatic API convention specification. The decision to support a second programming language is a commitment to maintain a separate codebase (or a separate output of a code generator) with the language's idiomatic conventions, in the language's package ecosystem, across the language's runtime version matrix, for the duration of the major version support window. The maintenance burden of a second language target is not proportional to the size of the API surface — it is proportional to the surface size multiplied by the number of language versions supported, the number of operating system targets the language runtime runs on, and the frequency of breaking changes in the language's dependency ecosystem.

Each supported language has community-established conventions for public API design that determine whether developers who use the language consider the SDK trustworthy. In Python, public method names are snake_case, type annotations use the typing module or built-in generic types (Python 3.10+), errors are raised as exceptions (not returned as result objects), async operations use async/await with an asyncio event loop, and response types are dataclasses, Pydantic models, or TypedDicts — not plain dictionaries, because plain dictionaries offer no static type checking. In JavaScript and TypeScript, method names are camelCase, async operations return Promises (or use async/await which desugars to Promises), and response types are TypeScript interfaces or type aliases. In Go, method names are PascalCase for exported identifiers, errors are returned as explicit return values (not panics), and response types are structs with exported fields.

The SDK ADR must document the idiomatic convention specification for each supported language as an explicit constraint on the SDK implementation: method naming (the exact casing convention and the rule for acronyms — is it get_api_key or get_a_p_i_key in Python, getAPIKey or getApiKey in TypeScript), type annotation policy (which response types are typed as typed dataclasses vs. plain dicts, whether the SDK exports the type definitions so consumers can import them, how nullable fields are typed), error handling model (a single exception base class with subclasses per error category, or a union error type, or both), async support model (sync-only, async-only, or sync-and-async with a separate async client class), and the pagination model (a method that returns a list plus a cursor, a generator that yields items across pages, or a class with a next_page() method). The API schema design decision record documents the API response envelope format; the SDK ADR must document how the SDK maps the API's envelope structure to language-native types — whether the data, meta, and errors envelope fields are exposed as separate properties on the response object or collapsed into a single return value for the common success case.

The decision of whether to hand-maintain the SDK or auto-generate it from an OpenAPI specification is part of the language target specification. The build vs. buy decision record documents the evaluation framework for this class of decision; for SDK generation specifically, the evaluation must address three questions: whether the OpenAPI specification is accurate enough to serve as the SDK source of truth (if the specification lags behind the API implementation, generated SDKs will contain methods that do not work), whether the code generator produces idiomatic output for the target language or requires a post-processing layer to fix naming and type mapping (if post-processing is required, the maintenance burden reduction from generation is lower than it appears — post-processing must be updated alongside the generator and the specification), and whether the generated SDK can include convenience methods and pagination helpers that are not expressible in the OpenAPI specification (if not, hand-maintained methods must be added alongside generated code, introducing a maintenance complexity the team did not anticipate).

Property 3: The SDK versioning guarantees and breaking change classification. Semantic versioning declares that a major version bump signals a breaking change, a minor version bump signals backward-compatible additions, and a patch version bump signals backward-compatible fixes. The definition does not specify what counts as a breaking change for an SDK beyond "change that is not backward compatible." For an SDK, backward compatibility is more restrictive than most teams intuit, because SDK consumers depend on the SDK's interface by name — they import a function and call it by its documented name, not by the underlying API endpoint path — and any change to that name is a runtime failure for consumer code that has not been updated.

The complete breaking change classification for an SDK must enumerate the specific categories of change that require a major version bump. Method or function rename: renaming client.events.list() to client.events.query() is a breaking change because consumer code references the old name, which no longer exists after the rename. Parameter rename: renaming the page_size parameter to limit is a breaking change for any consumer that passes the parameter as a keyword argument (client.events.list(page_size=50)), even if the positional position of the parameter is unchanged and the default value is unchanged. Return type field removal: removing the created_by field from the response type is a breaking change for any consumer that reads response.created_by. Return type field type change: changing created_at from a string (ISO 8601 timestamp) to a datetime object is a breaking change for any consumer that passes the value to a function expecting a string. Error type change: changing the exception raised from NotFoundError to ResourceNotFoundError is a breaking change for any consumer that catches NotFoundError by class name. Removal of a deprecated method before the deprecation window expires: even if the method was formally deprecated in a previous version, removing it before the documented window ends is a breaking change because the deprecation announcement established a timeline expectation that consumers relied on when deciding whether to migrate immediately or at their next release cycle.

Non-breaking changes that can be made in a minor version: adding a new optional method parameter with a backward-compatible default value; adding a new method or function; adding a new field to a response type (consumer code that reads only the fields it expects is unaffected by new fields); adding a new error subtype that is a subclass of an existing error type (consumer code that catches the parent type still catches it). The API deprecation strategy decision record documents the API's deprecation policy; the SDK deprecation policy must be at least as conservative as the API's — if the API guarantees a 12-month deprecation window before removing an endpoint, the SDK must guarantee at least 12 months before removing the corresponding method, and should guarantee longer to account for the time between the API deprecation and the SDK deprecation announcement, plus consumer migration time.

What the founding session records and what it omits

The SDK is almost always built in multiple phases: an initial phase when the first customer asks for a client library (driven by a specific enterprise integration requirement), a second phase when a second language is requested (driven by a different customer segment's preferred language), and a third phase when the SDK needs documentation beyond the README (driven by the support cost of questions from developers who are confused about the SDK's behavior). None of these phases produces an SDK ADR. Each phase produces a working artifact for the specific scenario that motivated it, leaving the broader contract — surface boundary, breaking change classification, language convention specification, deprecation policy — undocumented and discoverable only through the next incident.

Three types of AI chat sessions generate these gaps:

The "let's build a client library for our API" session. The engineer is responding to customer requests for an SDK and asks how to structure a client library that wraps the REST API. The session explains how to organize the client class, add authentication, implement retry logic, and publish to npm or PyPI. What the session does not ask or answer: which of the 40 API endpoints should be included in the SDK with a stability guarantee and which should be accessible only via the raw HTTP client; what the policy is for changes to endpoint behavior — if the API changes the shape of a response field, does the SDK automatically expose the new shape (breaking consumers who depended on the old shape) or does the SDK normalize the response to maintain backward compatibility; what the breaking change classification is for the SDK specifically — does renaming a convenience method that aggregates multiple API calls constitute a breaking change, and does adding a required field to a request parameter constitute a breaking change; what the deprecation notice period is before a method can be removed; and what the versioning policy is for pre-1.0 versions — many teams ship v0.x.x without a stability guarantee and then release v1.0.0 when they feel the SDK is stable, but without documenting when that stability guarantee begins, consumers cannot know whether ^0.9.0 will break on the next update. The package dependency management decision record documents how the team manages its own dependencies; the SDK's dependency management policy must also be documented — the SDK's dependencies (HTTP client library, JSON parser, authentication library) constrain the consumer's dependency graph; a consumer who depends on a specific version of the HTTP client library may experience conflicts when the SDK pins to a different version, and the SDK ADR must document the SDK's pinning policy (pin to a minimum version, pin to a range, or list the tested versions and mark others as unsupported).

The "add Python/Go/Ruby support" session. The engineering team has shipped a JavaScript SDK and a customer segment in a different language ecosystem is requesting native support. The engineer asks how to build a client library in the target language. The session explains the package structure, the PyPI or pkg.go.dev publishing workflow, and shows how to wrap the REST API in the target language's idioms. What the session does not ask or answer: the complete idiomatic convention specification for the target language — not just method naming, but type annotation policy, error handling model, async support model, and pagination model; how the new language SDK will be kept in sync with the JavaScript SDK as the API evolves — whether there is a single source of truth (the OpenAPI specification) that both SDKs are generated from, or whether they are maintained independently and will drift; what the minimum language version is that the SDK supports (Python 3.8 vs. 3.10 vs. 3.12 have meaningfully different typing capabilities, and the target must be documented so consumers know whether their runtime is supported); how the SDK will be tested across language versions — a Python SDK that passes tests on Python 3.12 may fail on Python 3.8 due to syntax or standard library differences; and whether the new language SDK is considered a first-class supported SDK with the same stability guarantee as the JavaScript SDK, or a community-level SDK with weaker guarantees. The test strategy decision record documents the testing approach; the SDK test matrix must document which language versions, which operating system targets, and which dependency versions are tested in CI, because a release that passes tests on macOS with Python 3.12 and fails on Linux with Python 3.8 will break customers who depend on the older runtime, and the matrix must be broad enough to catch those failures before the package is published.

The "let's write proper SDK documentation" session. The SDK has been live for several months and developer support questions are accumulating. The engineer asks how to structure SDK reference documentation. The session recommends generating documentation from type annotations and docstrings (Sphinx for Python, TypeDoc for TypeScript, godoc for Go), explains how to publish to a documentation hosting service, and shows the format for documenting each method. What the session does not ask or answer: how changelog entries are structured so consumers can evaluate whether an update contains a breaking change without reading the full diff (a changelog that lists breaking, deprecated, added, changed, fixed, and removed changes per version in a consistent format is the consumer's primary tool for deciding whether to accept an update); what the EOL announcement timeline is for a major version — if v2.0.0 ships in six months and v1.x.x will be supported for one year after the v2.0.0 release, when and how will consumers who are still on v1.x.x be notified; what the migration guide format is for a major version release — a list of every breaking change with a before/after code example for each is the minimum useful content; and whether the SDK documentation is versioned — if a consumer is still on v1.x.x, can they access the v1.x.x documentation, or does the documentation site show only the latest version (which may describe methods and behaviors that do not exist in the version they are running). The documentation strategy decision record documents the team's approach to product documentation; the SDK documentation versioning policy must be an explicit decision — hosting multiple versions of SDK documentation has a maintenance cost, but a documentation site that shows only the latest version forces consumers who cannot upgrade immediately to work without reference documentation for the version they are running, which increases support load and migration errors.

The WhyChose extractor surfaces the "build a JavaScript SDK" session, the "add Python support" session, and the "document the SDK" session from AI chat history. The SDK ADR converts the implicit choices in those sessions — which endpoints to wrap, how to name Python methods, how to handle pagination — into a documented stable public API surface boundary that specifies what changes trigger the versioning policy, a documented breaking change classification that specifies whether renaming a parameter is breaking (yes, always) or just a parameter rename at the positional level (not breaking if default is preserved), and a documented language-target idiomatic convention specification that prevents the next engineer who adds a third language from shipping camelCase method names in a Python SDK because no one wrote down that Python SDK methods must be snake_case.

The five sections of an SDK design ADR

Section 1: SDK scope and stable public API surface enumeration. Document the stable public SDK surface as an explicit list of resources and operations covered by the SDK's stability guarantee. Organize the list by resource (events, deployments, API keys, webhook endpoints, workspace members) and for each resource enumerate the operations that have SDK methods (create, get, update, delete, list, query), the operations that are available via the raw HTTP API only (batch import, bulk export, internal status check), and the operations that are internal-only and must not be called by consumers (internal metrics endpoint, debug state dump, internal migration trigger).

Document the stability tier for each surface area. Stable (covered by the full versioning policy — breaking changes require a major version bump with a deprecation window): the core CRUD operations for the primary resources. Beta (may change in minor versions with notice, but consumers who opt into beta methods accept a higher change rate): new features in early access. Experimental (may change or disappear in any release, no versioning policy applies): prototype features that are available for testing but not production use. The stability tier must be surfaced in the SDK itself — a beta method is annotated with a deprecation-like marker that signals its tier (a @beta decorator in Python, a @experimental JSDoc tag in TypeScript, a comment in the godoc for Go), so consumers who are reading the SDK documentation in their IDE see the stability tier without consulting the ADR.

Document the SDK surface boundary review process. When a new API endpoint is added, the SDK team must decide within one release cycle whether to add it to the stable surface, the beta surface, or make it available via the raw HTTP client only. The decision must be made before the API endpoint is promoted to stable — once the API team marks an endpoint as stable and announces it in the API changelog, consumer integrations will be built against it using the raw HTTP API; if the SDK team then adds the endpoint to the stable SDK surface with a different interface than the raw API (different parameter names, different response type), those consumers must migrate. The CI/CD pipeline decision record documents the release automation; the SDK release pipeline must include a step that compares the current SDK surface against the OpenAPI specification and flags any API endpoints that are marked stable but have no corresponding SDK method, so the SDK team cannot accidentally miss a promotion decision.

Section 2: Language targets and idiomatic API conventions per language. Document each supported language target with the idiomatic convention specification as an explicit constraint. For each language: the supported runtime version range (Python 3.9–3.12, Node.js 18 LTS and 20 LTS), the method naming convention (snake_case for Python, camelCase for JavaScript and TypeScript), the type annotation policy (required on all public methods for Python and TypeScript, optional for Go where the type system enforces type safety without explicit annotations), the error handling model (Python: exception base class SDKError with subclasses per error category — AuthenticationError, NotFoundError, RateLimitError, ValidationError, ServerError; TypeScript: rejected Promise with an error class hierarchy parallel to Python; Go: error return values using error types that implement the error interface), the async support model (Python: both sync client Client and async client AsyncClient using asyncio, with identical method signatures; TypeScript: async-only with Promises; Go: sync-only, with the option to call in a goroutine), and the pagination model (Python and TypeScript: generator that yields items across pages, with an optional all_pages=True parameter on list methods that materializes all pages into a list; Go: an iterator type with a Next() method).

Document the convention for method naming of acronyms, because acronym naming is the most common source of inconsistency across SDK methods. The recommended rule: treat acronyms as words, not as uppercase sequences. In Python: get_api_key (not get_a_p_i_key or getAPIKey), create_tls_certificate (not create_t_l_s_certificate). In TypeScript and JavaScript: getApiKey (not getAPIKey), createTlsCertificate (not createTLSCertificate). This rule matches Go's official style guide and produces consistent results when auto-generating SDK methods from OpenAPI operation IDs. Document the rule explicitly — without it, different engineers will make different choices for different acronyms, producing a mixed naming scheme that is inconsistent enough to confuse consumers but not consistent enough to allow a mechanical rename.

Document whether each language target's SDK is hand-maintained, generated, or generated with a hand-maintained override layer. If generated: the generator tool and version, the OpenAPI specification path and version that is the source of truth, the post-processing script that applies idiomatic overrides (renames, type mappings, convenience methods), and the policy for hand-maintained additions that supplement the generated code (pagination helpers, retry logic, authentication refresh). If hand-maintained: the mechanism for keeping the SDK in sync with the API specification (a test that calls every stable SDK method against a staging environment and verifies the response shape matches the SDK's type definitions, run in CI on every API deployment). The API contract testing decision record documents the contract testing approach; the SDK contract test must be included in that framework — a test that verifies the SDK's response types match the API's actual response shapes is the only mechanism that catches API drift before it reaches consumers.

Section 3: Versioning guarantees and breaking change classification. Document the complete breaking change classification as an explicit enumerated list. Breaking changes that require a major version bump: (1) renaming a public method or function; (2) renaming a required or optional positional parameter; (3) renaming an optional keyword parameter whose name consumers pass explicitly; (4) removing a field from a response type; (5) changing the type of a response field from a more permissive type to a more restrictive type (string to enum, any to a specific type, dict to a typed dataclass); (6) changing the exception or error type raised for a given error condition; (7) removing a deprecated method before the documented deprecation window has elapsed; (8) changing the semver range of a required dependency in a way that excludes versions consumer code currently depends on; (9) removing support for a previously supported runtime version (dropping Python 3.9 support when the SDK previously supported Python 3.9).

Non-breaking changes that can be made in a minor version: (1) adding a new optional method parameter with a default value that preserves existing behavior; (2) adding a new field to a response type (consumer code that reads only the fields it knows about is unaffected); (3) adding a new method or function; (4) adding a new error subtype that is a subclass of an existing error type (consumer code that catches the parent type still catches it); (5) adding support for a new runtime version; (6) widening a parameter type from a more restrictive type to a more permissive type (adding new accepted values to an enum parameter, accepting a string where previously only an enum value was accepted). Patch changes: (1) fixing a bug where an SDK method produces an incorrect result without changing its interface; (2) improving error messages without changing the error type or adding new error conditions; (3) performance improvements to the underlying HTTP client that do not change the observable interface.

Document the semver policy for pre-1.0 versions. The semver specification permits breaking changes in any 0.x.y release, but consumers who adopt a pre-1.0 SDK in production cannot accommodate arbitrary breaking changes at arbitrary frequency. The SDK ADR must specify whether the pre-1.0 minor version (0.x) carries a breaking change guarantee: either (a) the SDK makes no breaking change guarantee before 1.0.0 and consumers who adopt it in production do so at their own risk (this must be prominently stated in the README, not buried in the ADR), or (b) the SDK applies the same breaking change policy as post-1.0 even in 0.x versions, treating 0.x increments as if they were major version increments. The API versioning decision record documents the REST API versioning policy; the SDK versioning policy must be consistent with the API versioning policy — an API that guarantees no breaking changes within a major version cannot be correctly wrapped by an SDK that permits breaking changes in minor versions.

Section 4: SDK distribution, installation, and release pipeline. Document the package registry for each language target and the installation command consumers will use: npm for JavaScript and TypeScript (npm install @company/sdk), PyPI for Python (pip install company-sdk), pkg.go.dev for Go (go get github.com/company/sdk-go@v1.2.0), Maven Central for Java (implementation 'com.company:sdk:1.2.0'). Document the namespace and package name decision: the npm scoped package namespace (@company/) prevents name squatting; the PyPI package name must be unique globally and should use a namespace prefix (company-sdk) to avoid conflicts with community packages that use generic names. Document the release trigger and authorization model: who can trigger a release (only the SDK team, any repository contributor with a specific GitHub Actions permission, automated release on API change), what the release process is (git tag triggers CI pipeline that runs the full test matrix, builds the package, and publishes to the registry after all tests pass), and what the authorization model is for publishing (a dedicated publishing token with the minimum required permissions, stored as an encrypted CI secret, rotated on the SDK deprecation policy's major version cycle).

Document the test matrix that must pass before a release is published. The test matrix specifies the runtime versions and operating system combinations that are tested in CI: for Python, the supported Python versions (3.9, 3.10, 3.11, 3.12) on Linux x86-64 and macOS ARM64; for Node.js, the active LTS versions (18, 20, 22) on Linux x86-64; for Go, the two most recent stable releases on Linux x86-64 and macOS ARM64. The matrix must be wide enough to catch version-specific failures before the release — a Python SDK that uses a walrus operator (:=) will fail to import on Python 3.7 and raise a SyntaxError; a TypeScript SDK that uses satisfies operator (TypeScript 4.9+) will fail to compile on TypeScript 4.8. The CI/CD pipeline decision record documents the pipeline infrastructure; the SDK test matrix must be a first-class configuration in the CI pipeline, not an afterthought in a local Makefile target that is only run when a developer remembers to execute it before release.

Document the release automation dependency: whether the SDK release pipeline depends on the API's OpenAPI specification being tagged and released alongside the SDK, or whether the SDK is released independently of the API release cycle. An SDK that is released independently may lag behind the API — methods for new endpoints are not available until the SDK team manually adds them and releases — or may lead the API — methods for endpoints that are in development appear in the SDK before the API supports them in production. The recommended model for hand-maintained SDKs is independent release cycles with a maximum lag policy: the SDK team commits to releasing new endpoint methods within two weeks of the API endpoint reaching stable, and the API team commits to notifying the SDK team of endpoint promotions at least one week before they go stable. Document the notification mechanism — a Slack channel, a GitHub label on API pull requests, a field in the OpenAPI operation metadata — so the commitment is enforceable without requiring SDK team members to monitor all API changes manually.

Section 5: SDK deprecation policy and major version lifecycle. Document the deprecation process for individual methods. A method is deprecated by adding a deprecation marker (a @deprecated JSDoc tag in TypeScript, a warnings.warn(..., DeprecationWarning, stacklevel=2) call in the method body in Python, a // Deprecated: use NewMethodName instead. comment in Go) that surfaces in the consumer's IDE as a warning when they reference the deprecated method. The deprecation marker must include: the version in which the method was deprecated, the minimum version in which it will be removed, and a reference to the replacement method or the migration guide. The deprecation window — the minimum duration between the deprecation announcement and the removal — must be documented in the SDK ADR as a specific duration or version count: recommended minimum is two major versions or 12 calendar months, whichever is longer. A deprecation window shorter than two major versions means consumers who choose to upgrade major versions at a measured pace (one major version per quarter) may find a method removed before they had an opportunity to migrate in the window between the deprecation and the removal.

Document the major version lifecycle policy: the support window for a major version after the next major version ships. The support window specifies the minimum duration during which the team will backport security fixes to the previous major version (recommended: 12 months from the day the next major version reaches stable), the types of changes that are backported (security fixes only, or security fixes plus critical bug fixes), and the EOL announcement timeline (the date on which the previous major version reaches end-of-life must be announced at least six months before the EOL date so consumers have time to plan and execute the upgrade). Document the migration guide format: each breaking change in a major version must have a before/after code example that shows the old method call and the equivalent new method call, alongside a brief explanation of why the change was made. A migration guide that lists breaking changes without code examples forces consumers to reverse-engineer the migration from the changelog entry and the new documentation, which increases migration errors and support requests.

Document the EOL communication channels: where the EOL announcement will be published (the package registry page, the documentation site, a pinned GitHub issue, the developer newsletter), what the in-SDK notification mechanism is (a runtime warning emitted on import for SDKs that are within 90 days of EOL: DeprecationWarning: SDK v1.x.x reaches end-of-life on 2027-06-01. Upgrade to v2.x.x to continue receiving security fixes.), and what happens to the package registry listing after EOL (the package remains available and installable — removing it from the registry would break builds that pin to the EOL version — but the README is updated with an EOL banner and the documentation site's version selector marks the EOL version with a warning). The documentation strategy decision record documents the documentation hosting infrastructure; the SDK documentation versioning policy must specify how many major versions of documentation are hosted simultaneously, and what happens to the documentation for a major version after it reaches EOL — the recommended approach is to keep it hosted indefinitely but add a prominent banner stating the EOL date and linking to the migration guide for the current major version, because removing documentation for an EOL version forces consumers who are still running it (for whatever operational reason) to debug against no reference, which increases the probability of a security vulnerability going unpatched due to migration friction.

None of these deprecation and lifecycle structures — the deprecation window, the major version support period, the migration guide format, the EOL announcement timeline, the in-SDK runtime warning — are configured in the founding session that builds the client library. The founding session produces an SDK that works for the current customer integrations. The SDK ADR produces the operational model that covers the full lifecycle of the SDK — the point at which a method becomes deprecated, the minimum time before it can be removed, the communication path to consumers whose code will break if they do not migrate, and the evidence trail that the team can point to when a customer says "you broke my integration" and the team can respond with the deprecation announcement date, the removal announcement date, and the migration guide that was available throughout the deprecation window. Without the ADR, the answer to "you broke my integration" is "we updated the changelog."

FAQs

What constitutes a breaking change in an SDK, and what changes can be made in a minor or patch version?

A breaking change in an SDK is any change that can cause consumer code that was working on the previous version to fail to compile, fail to run, or produce incorrect behavior on the new version without the consumer making any changes. The key insight is that SDK consumers reference the SDK's interface by name — they call a method by its documented name, catch an exception by its class name, and read a response field by its field name — and any change to those names is a runtime failure for code that has not been updated.

Breaking changes include: renaming a public method or function (the old name no longer exists); renaming a parameter that consumers pass by keyword argument; removing a field from a response type; changing the type of a response field; changing the exception type raised for an error condition; removing a deprecated method before the documented deprecation window expires. Non-breaking changes that can be made in minor versions include: adding a new optional method parameter with a backward-compatible default value; adding a new field to a response type; adding a new method; adding a new error subtype that is a subclass of an existing error type. Patch versions cover bug fixes that correct behavior to match the documented specification without changing signatures or response shapes. The SDK ADR must document this classification explicitly — the most commonly contested case is parameter rename, and the answer is always "yes, it is breaking" for any parameter that consumers reference by name.

Should SDKs be hand-maintained or auto-generated from an OpenAPI specification?

Auto-generation from an OpenAPI specification reduces maintenance burden significantly: new API endpoints appear in the SDK without a separate SDK pull request, the specification is the source of truth, and SDK-API drift is structurally prevented. The cost of auto-generation is constrained idiomatic quality — generators map OpenAPI operation IDs to SDK method names directly, which produces camelCase method names in a Python SDK if the OpenAPI spec uses camelCase operation IDs, and produces non-idiomatic type mappings that the language's static analysis tools will flag.

Hand-maintained SDKs can be fully idiomatic and include convenience methods (pagination helpers, retry logic, request builders) that generated code cannot express. The cost is maintenance: every API change requires a corresponding SDK update, and SDKs can lag behind the API under delivery pressure. The correct choice depends on API surface size and change rate. For an API with fewer than 50 endpoints that changes slowly, hand-maintained SDKs in two or three languages are manageable. For an API with 200+ endpoints that changes frequently across multiple teams, auto-generation with idiomatic post-processing hooks is the practical choice — but the post-processing layer and the idiomatic override specification must be documented in the SDK ADR, because without documentation the post-processing layer will be modified inconsistently by different engineers and will accumulate exceptions that are as difficult to maintain as the hand-maintained alternative.

What should an SDK ADR document that the founding "let's build a client library" session does not?

The founding session produces a working wrapper for the API endpoints that exist at that moment, published to the package registry, with enough documentation to get a consumer started. An SDK ADR must document five structural properties. (1) The SDK scope and stable public API surface: which resources and operations are included in the stable surface with a versioning guarantee, which are available via raw HTTP API only, and which are internal-only — the boundary that determines what changes trigger the versioning policy. (2) The language targets and idiomatic API conventions: for each supported language, the method naming convention, type annotation policy, error handling model, async support model, pagination model, and whether the SDK is hand-maintained or generated with post-processing. (3) The versioning guarantees and breaking change classification: the complete list of changes that require a major version bump (including the commonly missed case of keyword parameter renames), the list of non-breaking changes, and the semver policy for pre-1.0 versions. (4) The distribution and release pipeline: the package registry for each language target, the test matrix that must pass before release, and the release trigger and authorization model. (5) The deprecation policy and major version lifecycle: the deprecation window expressed as a version count and calendar duration, the major version support period, the migration guide format, and the EOL announcement timeline and communication channels.

None of these appear in the founding session. All of them determine whether the SDK is a trusted stability surface that consumer integrations can depend on across major version transitions, or a liability that breaks 40 customer integrations when a developer renames a method because no one wrote down that method renames are breaking changes.