Skip to content

Repositories, gateways, clients, and ports and adapters

A port states what the application needs from an external capability. An adapter translates that contract to SwiftData, files, URLSession, CloudKit, or a deterministic local implementation.

Name the application purpose

protocol NoteLibrary: Sendable {
func notes(matching query: NoteQuery) async throws -> [Note]
func note(id: UUID) async throws -> Note?
func save(_ note: Note) async throws
func delete(id: UUID) async throws
}

NoteLibrary describes Field Notes. SwiftDataRepositoryProtocol would expose a vendor choice and encourage persistence details to leak into callers.

Make adapters honor one contract

An in-memory adapter should preserve identity, validation, ordering, replacement, and read-after-write behavior. A SwiftData adapter maps persistent models to domain values and translates storage failures into application errors. Contract tests run the same scenarios against both.

Different names signal different responsibilities:

  • a repository or library offers collection-like domain access
  • a gateway wraps an external business capability
  • a client handles a transport protocol such as HTTP
  • an adapter translates between an application port and one technology

Names matter less than clear ownership and dependency direction.

Do not protocolize everything

A concrete pure formatter needs no interface just because it is concrete. Add a port when there is a volatile integration, useful local substitute, test distance improvement, or policy boundary. Keep transactions and multi-step consistency in one owner rather than leaking a sequence of primitive calls to every use case.

Series navigation

References