How do you design a public Swift type: struct vs class vs actor vs enum?

1.1.5 · L8 · Interview

Predict first

Your library changes public struct Config to public class Config. What happens to this client code?

var a = Config()   // timeout = 10
var b = a
b.timeout = 30
print(a.timeout)
  • Fails to compile
  • Still prints 10
  • Compiles, now prints 30

Mental model. Pick the ownership story first, then the keyword. The keyword is the easy part; the ownership story is the API.

Start with ownership. If copies should be independent → struct or enum. If there is one world object → class. If mutation must be serialized → actor, or a struct with a single isolated owner. If the set of states is closed → enum.

Then freeze that choice in the public API: flipping struct → class later is a source break, and if the type is in a resilient library it can also be an ABI break. Document isolation and sendability on the type, not in a wiki.

Also decide what you are exposing: a concrete type, a protocol, an opaque some P, or an existential any P. Prefer the narrowest surface that lets clients do their job.

No compiler error warns the client: the same code silently changes meaning. That is why the struct/class choice is part of the public contract and has to be made up front.

Common traps

  • Designing for imagined future subclassing. Open-by-default classes cost you devirtualization and force you to support overrides forever.
  • Leaking the implementation type through some. Opaque types are great, but you cannot change the underlying type without a source break if clients rely on properties you expose.

They might follow up with

  • What if you need to add a stored property later? If the type is resilient (non-@frozen) that can be ABI-compatible. If it is @frozen, it is not.
  • How do you deprecate a type? @available(*, deprecated, message:) plus a replacement, and never silently change layout or semantics in a patch release.

Related questions

  • 1.1.4 When would you still use a class in a Swift-first codebase?
  • 1.3.8 How do you evolve a public protocol without breaking clients?
  • 1.10.4 What is ABI stability vs source compatibility vs module stability?
  • 1.10.6 You need to add a field to a public struct in a binary SPM library used by five apps. What do you consider?

← When would you still use a class in a Swift-first codebase?   When is teaching “always use structs” harmful on a team? →