How do you design an SDK or shared module for other teams: surface area, configuration, errors, concurrency, dependencies, documentation and support?
Predict first
A module has a public client and protocol plus an internal helper, an internal property and an internal type. Which symbols appear in its public symbol graph?
// === main.swift ===
public struct Client {
public init() {}
public func fetch() {}
func helper() {}
internal var cache = 0
}
public protocol Plugin { func run() }
struct Hidden {}
- Client, Plugin, fetch(), init() and run() only
- All symbols, including the internal ones
- Only the protocol
Mental model. A good SDK is a power outlet: a small, standard socket that safely hides the grid behind it. Customers should never need to know about the wiring, and you can rewire the wall without changing the plug.
An internal SDK is a product whose customers are other engineers, and the design rules follow from that. Keep the public surface small: everything public is a promise you must keep (Q360), so default to internal and expose only what consumers need; the check below generates the module’s public symbol graph (the same data that documentation tools use) and shows that only the public client, its method and the protocol appear while helper, the internal cache and Hidden do not. No hidden global state: avoid singletons and implicit setup; take configuration through an initialiser or a single configure call, and allow several instances so tests and multi-account apps work. Explicit errors and concurrency: a documented, typed error model that consumers can switch over (Q358), async APIs with clear cancellation behaviour, Sendable types and stated actor isolation (does it call back on the main actor or any thread?), because an undocumented threading contract is the most common source of consumer bugs. Few dependencies: every dependency you add is imposed on every consumer and can conflict with theirs; wrap or avoid third-party code in a public API.
Design for consumers’ tests: expose protocols or a test double so apps can fake the SDK without networking, and ship a sample app that doubles as an integration test. Provide documentation as part of the deliverable: DocC articles and symbol documentation, quick-start, migration guides and a changelog; decisions and trade-offs in ADRs. Respect the platform: a privacy manifest and required-reason declarations (Q345), a stated size and launch-time cost, no work at +load or on the main thread at startup (Q221), and distribution as a source package or binary XCFramework with library evolution when you need ABI stability across compilers (Q366, Q198). Versioning and deprecation follow semantic versions with a published policy, deprecation warnings with replacements before removal, and a compatibility matrix across app versions (Q360, Q487). Offer support: an owner, a channel, an SLA for bugs, telemetry on adoption (which versions are live), and a path for contributions. The senior question to ask for every API is: if I need to change this in a year, how many teams do I have to coordinate?
Only public declarations are part of the contract (and of generated documentation), so keeping the rest internal keeps the surface small.
Common traps
- Making everything
public“in case someone needs it”. Each exposed symbol becomes a compatibility promise. - An undocumented threading contract. Consumers guess the callback queue and ship race conditions.
They might follow up with
- How do you give consumers testability? Protocols or fakes for the SDK, a sample app and clear initialisation without global state.
- What do you document besides method signatures? Threading and error contracts, quick-start, migration and deprecation policy.
Related questions
- 26.3.1 How do you architect several brands or white-label apps from one codebase: configuration, theming, build…
- 26.4.4 How do you think about a platform organisation and multi-year technical bets: charter, paved road, build…
- 5.4.1 What does -enable-library-evolution change, and why would you not turn it on for an app?
- 14.3.1 What is a privacy manifest, what are required-reason APIs, and what must third-party SDKs declare?
- 15.4.1 How do you model errors in Swift: throws, Result, optionals and typed throws, and how do you translate them…
- 15.5.2 How do you evolve a public API without breaking callers: deprecation, renames, versioning and source versus…
- 16.2.2 What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a…
- 25.4.2 How do you run a release train for many apps: cadence, branch cuts, ownership, go/no-go criteria, hotfix…
← How do you architect several brands or white-label apps from one…