Skip to content
← Back to architecture/ SDK Architecture

SDK Architecture

The Headless and UI Split

Ship an SDK as two layers: a headless core carrying logic, state, and API contracts, and a UI layer built on top of it. Clients take both or take only the core, without either path being second class.

SDK DesignAPI DesignDesign SystemsCross-Platform
ONE CORE, TWO INTEGRATION PATHS UI layer Components, layout, interaction Theme tokens Renders. Owns no logic. Headless core API contracts, auth, state Validation, retry, idempotency Renders nothing. DEPENDS ON NEVER Client A: moves fast Takes both layers Ships in days Client B: design led Takes the core only Brings its own interface

The problem shape

Reach for this when one SDK has to serve two populations that want opposite things.

The first group wants to move fast. They have no design team, they are integrating because a deal requires it, and every component you hand them is a week they do not spend building. They want the whole thing.

The second group has a mature design system and strong opinions. Your components will never match their brand, and asking them to adopt your UI means asking them to ship something that looks foreign inside their own product. They want the logic and none of the interface.

Serve only the first and you lose the second entirely, because they cannot use what you built. Serve only the second and every integration becomes a multi-month project. The usual compromise, one component library with enough theming hooks to satisfy everybody, satisfies neither: it is too rigid for the design-led team and too configurable for the team that wanted defaults.

The structure

Two published packages with a strict dependency direction.

The headless core owns everything that is true regardless of how it looks: API contracts and transport, authentication and session lifecycle, state and caching, validation rules, retry and idempotency, and the domain state machines. It exposes state and actions. It imports no UI framework and renders nothing.

The UI layer owns presentation only: components, layout, interaction affordances, and the token system that themes them. It consumes the core through the same public interface a client would use.

The rule that makes it work is that the dependency arrow points one way and never back. The core must never import from the UI layer, and must never grow an API shaped by what a component happens to need. The moment it does, the core stops being independently usable and the second population loses their path.

A useful test: could the core be consumed by a command line tool, a background job, or a different UI framework entirely, without modification? If not, presentation has leaked into it.

Trade-offs

You maintain two packages, two versioning stories, and a compatibility matrix. The core and the UI layer will drift in release cadence, and you need an explicit policy for which core versions a given UI version supports.

The public surface is larger. Both layers are now APIs that clients depend on, so both are subject to the same deprecation discipline. The core's interface in particular becomes very hard to change once design-led clients have built against it.

Documentation doubles. Two integration paths means two getting-started guides, two sets of examples, and a decision point at the top that clients have to be helped through.

The payoff is that both populations get a first-class path, and the expensive part of the SDK, the logic, is written once. It also front-loads a discipline that pays off later: because the core cannot depend on the UI, it stays testable without a rendering environment, which makes the whole system cheaper to verify.

When not to reach for it

When you only have one kind of client. If every integrator wants the components, the split is pure overhead. Ship one package and revisit if a design-led client appears.

Before you have integrators at all. This is a response to a real tension between real consumers. Building it speculatively means designing a boundary against imagined requirements, and the boundary will be wrong in ways you cannot detect without users.

When the UI is the product. If what you are selling is a specific interface experience, splitting it out invites clients to discard the thing that differentiates you.

When the team cannot maintain two packages. Two packages need release tooling, a versioning policy, and someone who owns the compatibility matrix. Without that, the split degrades into one maintained package and one that quietly falls behind, which is worse than never having split.