All blog posts

Design study · proposed · System Architecture · Financial Tech

Keeping SDKs consistent across iOS, Android, and web

Changing one API across several public SDKs is where maintenance debt shows up. This design study walks a hypothetical change through a shared contract, generated clients, and platform packs, and when separate implementations stay simpler.

4 min read · Updated 2026-04-23

Reader takeaway

  • Four kinds of “shared” kept separate (runtime, contract, clients, wrappers)
  • One hypothetical API change walked from OpenAPI to release
  • When a shared core is worth the coordination cost, and when it is not

The maintenance tax

When partners embed checkout, ads, or rewards, you often ship SDKs for iOS, Android, and web. Change one public method and you may touch three codebases, three release trains, and three sets of samples. Without a shared source of truth, APIs drift, bugs fork, and every platform fix costs another pass.

Status: proposed design for a commerce SDK stack. Not a shipped multi-platform release with measured maintenance outcomes.

Four kinds of “shared”

Teams blur these together. Keep them separate:

KindWhat it isExample
Shared runtimeCode that runs on the device in every packSession validation, checkout state machine in a Core SDK
Shared contractMachine-readable API surfaceOpenAPI for POST /intents
Generated clientsCode produced from the contractTypeScript / Swift / Kotlin stubs
Platform wrappersThin idiomatic bindings over the coreSwift Checkout.start() calling Core

The proposal mixes them: a Core SDK owns shared namespaces (session, ads, checkout, rewards); platform packs import the core; storefront connectors sit above when a partner needs a plugin shape; cloud APIs stay behind OpenAPI; webhooks close payment and reward events.

Day-to-day integration behavior is in How checkout fits into a partner app. This piece is about keeping platforms aligned over time.

Architecture from device Core SDK through gateway and service APIs into databases and event streams
Ownership map: device packs, gateway, services, and event consumers each need a clear check before release.

Worked example: add `metadata` to checkout start

Hypothetical change: partners need to pass a metadata map when starting checkout so support can find the cart later.

  1. Contract: add an optional metadata object on CheckoutStartRequest in OpenAPI.
  2. Generate: regenerate clients so each language gets the field.
  3. Core: accept and forward metadata in the shared checkout start path; do not re-implement per platform.
  4. Wrappers: expose idiomatic setters only if the language needs them (Swift dictionary vs Kotlin map). No forked validation rules.
  5. Tests: contract test that unexpected keys are rejected or stripped per policy; one platform integration test that the field round-trips.
  6. Review: human review if the field is customer-visible or changes breaking semantics.
  7. Release: version bump per platform policy; changelog names the field.
# OpenAPI sketch (hypothetical)
CheckoutStartRequest:
  properties:
    metadata:
      type: object
      additionalProperties:
        type: string

Deterministic generation from the schema is different from asking a coding agent to “update all SDKs.” Agents can open the PR, draft the changelog, and regenerate clients. People still decide breaking changes. If generation is wrong, fix the contract or generator, not only one platform pack.

What humans still review

Did the contract change intentionally? Do platform packs only add bindings, not forked business rules? Do tests cover double-submit and missing webhook cases you care about? Is the changelog honest about breaks?

Shared code reduces duplicated logic. Platform differences, release coordination, and dependency constraints remain. Test platform-specific behavior on purpose.

When sharing is worth it

Share a core when the same product rules must stay aligned across platforms and you can afford contract discipline. Prefer separate implementations when platforms diverge heavily, the team is small, or a shared core would become an abstraction that slows every change.

A sensible first slice of this design: one Core plus one platform pack shipping from the same contract, a documented human gate for breaking changes, and a regenerate-and-review path you can run twice without inventing a new process.