Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Last active June 18, 2026 20:22
Show Gist options
  • Select an option

  • Save JadenGeller/e9b109dc9d0f508d7d8d40151d2d58bf to your computer and use it in GitHub Desktop.

Select an option

Save JadenGeller/e9b109dc9d0f508d7d8d40151d2d58bf to your computer and use it in GitHub Desktop.
Service Linker Pattern

The Service Linker Pattern

A way to thread dependencies through your app using Swift's protocol system.

The Problem

You have services that need other services:

struct OrderService {
    func create(userId: String, items: [Item]) -> Order {
        // Need to look up user... somehow
        // Need to check inventory... somehow
        // Need to charge payment... somehow
    }
}

Option 1: Singletons. Easy to write, impossible to test.

let user = UserService.shared.find(userId)  // Good luck mocking this

Option 2: Pass everything explicitly. Safe, but tedious.

struct OrderService {
    let userService: UserService
    let inventoryService: InventoryService
    let paymentService: PaymentService
    let db: Database
    
    init(userService: UserService, inventoryService: InventoryService, 
         paymentService: PaymentService, db: Database) {
        // ...
    }
}

// Somewhere at the root of your app:
let db = PostgreSQL()
let userService = UserService(db: db)
let inventoryService = InventoryService(db: db)
let paymentService = PaymentService(stripe: stripe, db: db)
let orderService = OrderService(
    userService: userService,
    inventoryService: inventoryService,
    paymentService: paymentService,
    db: db
)
// ... repeat for every service

Every new service means more wiring. Every new dependency means updating constructors throughout the chain.

What we want: Pass one thing, get access to everything you need, but only what you declare.

The Core Trick

Here's the fundamental move:

// 1. Declare "I need a database"
protocol HasDatabase {
    var db: Database { get }
}

// 2. Write a service that works with ANY type that has a database
struct UserService<Deps: HasDatabase> {
    let deps: Deps
    
    func find(id: String) -> User? {
        deps.db.query("SELECT * FROM users WHERE id = ?", id)
    }
}

// 3. Create a bundle with a database
struct App: HasDatabase {
    let db = PostgreSQL()
}

// 4. It just works
let app = App()
let userService = UserService(deps: app)
userService.find(id: "123")

UserService doesn't know what Deps is. It just knows it has a db property. You could pass it App, or TestApp, or anything with a database.

This is the Reader pattern: instead of passing Database to every function, you pass an "environment" once and pull what you need from it.

Adding More Services

Now let's add OrderService that needs UserService:

protocol HasUser {
    var user: UserService<Self> { get }
}

struct OrderService<Deps: HasDatabase & HasUser> {
    let deps: Deps
    
    func create(userId: String, items: [Item]) -> Order {
        guard let user = deps.user.find(id: userId) else {
            throw OrderError.userNotFound
        }
        // ...
    }
}

But wait—how does App get a user property? We could add it manually:

struct App: HasDatabase, HasUser {
    let db = PostgreSQL()
    var user: UserService<App> { UserService(deps: self) }
}

This works. But now every bundle needs to manually wire every service. We're back to boilerplate.

The Second Trick: Protocol Extensions

Swift lets us provide default implementations via protocol extensions:

protocol HasUser: HasDatabase {
    var user: UserService<Self> { get }
}

extension HasUser {
    var user: UserService<Self> { UserService(deps: self) }
}

Now ANY type that conforms to HasUser automatically gets a user property, as long as it also has HasDatabase (which HasUser inherits).

struct App: HasUser {  // Just declare the conformance
    let db = PostgreSQL()
}

App().user.find(id: "123")  // user property comes from the extension

The wiring happens automatically through the protocol extension. self in the extension is App, so UserService captures App as its deps.

The Problem With That

We've welded HasUser to a specific implementation. What if we want MongoUserService in production and MockUserService in tests?

// These can't both exist:
extension HasUser {
    var user: UserService<Self> { UserService(deps: self) }  // Postgres
}
extension HasUser {
    var user: MockUserService { MockUserService() }  // Mock
}

We need to separate "I have user capabilities" from "use this specific implementation."

The Third Trick: Link Protocols

Split it into two protocols:

// Abstract: "I have user capabilities"
protocol HasUser {
    associatedtype UserImpl: UserServiceProtocol
    var user: UserImpl { get }
}

// Concrete: "Use PostgresUserService for HasUser"
protocol LinkPostgresUser: HasUser, HasDatabase { }
extension LinkPostgresUser {
    var user: PostgresUserService<Self> { PostgresUserService(deps: self) }
}

// Alternative: "Use MockUserService for HasUser"
protocol LinkMockUser: HasUser { }
extension LinkMockUser {
    var user: MockUserService { MockUserService() }
}

Now bundles choose their implementation by which Link they conform to:

struct App: LinkPostgresUser {
    let db = PostgreSQL()
}

struct TestApp: LinkMockUser {
    // No database needed—mock doesn't use one
}

This is the "linking" step. HasUser is like a header file declaring a symbol exists. LinkPostgresUser is like -lpostgres telling the linker which library provides it.

What's Actually Happening

Three mechanisms working together:

1. Structural typing via protocol composition

Deps: HasDatabase & HasCache

This means "any type with db and cache properties." No specific type required. The shape is the contract.

2. Environment capture

struct UserService<Deps: HasDatabase> {
    let deps: Deps  // Captured once at construction
    
    func find(id: String) -> User? {
        deps.db.query(...)  // Accessed implicitly thereafter
    }
}

Services are functions from Deps → Capabilities. The struct just gives you named access to the captured environment.

3. Late binding via conformance

extension LinkPostgresUser {
    var user: ... { PostgresUserService(deps: self) }
}

The extension doesn't know what self is—the conforming type decides. Implementation is chosen when you write struct App: LinkPostgresUser, not at the call site.

Automatic Sharing

Here's something subtle. If two services both need a database:

struct App: LinkUser, LinkOrder {
    let db = PostgreSQL()
}

Both LinkUser and LinkOrder extensions receive self = App. When UserService accesses deps.db and OrderService accesses deps.db, they get the same instance.

No configuration. Same property name = same instance. The sharing is structural.

Scoped Visibility

Each service only sees what it declares:

struct UserService<Deps: HasDatabase & HasCache> {
    let deps: Deps
    // Can access: deps.db, deps.cache
    // Cannot access: deps.payment (even if App has it)
}

struct OrderService<Deps: HasUser & HasInventory> {
    let deps: Deps
    // Can access: deps.user, deps.inventory
    // Cannot access: deps.db directly (that's User's concern)
}

OrderService uses UserService but doesn't know UserService uses a database. Transitive dependencies are hidden. If UserService later moves to a microservice, OrderService doesn't change.

The Full Pattern

Four concepts:

Concept Purpose Example
Service The capability interface protocol UserService { func find(id:) -> User? }
Needs What's required HasDatabase, HasCache (compose them)
Impl How it works struct PostgresUser<D: HasDatabase & HasCache>
Link Which impl to use protocol LinkPostgresUser + extension

A complete service:

// Service: what it does
protocol UserService {
    func find(id: String) -> User?
    func create(email: String) async throws -> User
}

// Needs: what's required (atomic, composable)
protocol HasDatabase {
    associatedtype DB: Database
    var db: DB { get }
}

// Impl: how it works
struct PostgresUser<Deps: HasDatabase>: UserService {
    let deps: Deps
    
    func find(id: String) -> User? {
        deps.db.query("SELECT * FROM users WHERE id = ?", id).first
    }
    
    func create(email: String) async throws -> User {
        let user = User(id: UUID().uuidString, email: email)
        try await deps.db.insert("users", user)
        return user
    }
}

// Link: selects this implementation
protocol HasUser {
    associatedtype U: UserService
    var user: U { get }
}

protocol LinkPostgresUser: HasUser, HasDatabase { }
extension LinkPostgresUser {
    var user: some UserService { PostgresUser(deps: self) }
}

Using It

// Production
struct App: LinkPostgresUser, LinkStandardOrder, LinkStripePayment {
    let db = PostgreSQL()
    let stripe = StripeAPI(key: productionKey)
}

// Test: real database, mock payment
struct TestApp: LinkPostgresUser, LinkStandardOrder, LinkMockPayment {
    let db = PostgreSQL.testContainer()
}

// View only knows about capabilities, not implementations
struct CheckoutView<Services: HasUser & HasOrder>: View {
    let services: Services
    
    var body: some View {
        Button("Checkout") {
            let user = services.user.current()
            let order = services.order.create(userId: user.id, items: cart)
        }
    }
}

Why Not Just Functions?

You could thread dependencies as function parameters:

func findUser(db: Database, id: String) -> User?
func createOrder(findUser: (String) -> User?, db: Database, items: [Item]) -> Order

Problems:

  • createOrder needs findUser AND db. But findUser already uses db internally. You're passing db twice.
  • 10 services × 5 methods = 50 functions to wire manually.
  • To mock findUser, you rewire everything that captured it.

The bundle solves this. deps.db in UserService and deps.db in OrderService are the same because deps is the same object. Sharing is automatic. Mocking is surgical—swap one Link, everything else stays real.

Why Not a DI Container?

Runtime DI containers work fine. They're easier to learn but:

  • Errors at runtime, not compile time
  • Can't see what depends on what in the code
  • Overhead from runtime lookups

The Service Linker gives you compile-time checking. If a dependency is missing, it won't build. The wiring is visible in the type signatures.

Tradeoff: you learn the protocol mechanics upfront, but then the compiler catches mistakes forever.

When To Use This

Don't bother if:

  • You have < 10 services (just wire manually)
  • Team isn't comfortable with Swift generics/protocols
  • You want minimal concepts to learn

Consider it if:

  • Service graph is complex and growing
  • You want surgical test mocking (swap one thing, keep rest real)
  • You care about compile-time safety
  • You're okay with upfront learning cost

Zero Runtime Cost

Because everything is concrete at compile time:

// You write
app.user.find(id: "123")

// Compiler sees
PostgresUser<App>(deps: app).find(id: "123")

// After inlining
app.db.query("SELECT * FROM users WHERE id = ?", "123").first

The abstraction exists in source code. The binary is the same as if you wrote it by hand.

Summary

The Service Linker Pattern is:

  • Reader pattern: capture environment once, access implicitly
  • Structural typing: protocol composition describes the shape
  • Late binding: conformance chooses implementation

You get automatic dependency threading, automatic sharing, scoped visibility, and compile-time safety. You pay with protocol boilerplate and conceptual overhead.

It's not magic—it's just protocols and generics, composed carefully.

@JadenGeller

JadenGeller commented Jun 18, 2026

Copy link
Copy Markdown
Author
//  ═══════════════════════════════════════════════════════════════════════════
//  Kiosk.swift — the Service Linker pattern, end to end, in one literate file
//  ═══════════════════════════════════════════════════════════════════════════
//
//  A coffee-shop point-of-sale: a menu, open tabs, ingredient stock, a payment
//  gateway, a clock, and a write-ahead journal. Small enough to read in one
//  sitting, complete enough to exercise every part of the pattern:
//
//    Part 1  State slices            — plain copyable data, one per domain
//    Part 2  Capabilities            — borrow = read right, mutate = write right
//    Part 3  Reads                   — logic as constrained extensions
//    Part 4  Commands                — writes with compiler-checked footprints
//    Part 5  The substitution tier   — gateways, clocks, and their Links
//    Part 6  Resources               — unique I/O handles behind capabilities
//    Part 7  The World               — the composition root (the ONLY file tier
//                                      allowed to name it)
//    Part 8  Snapshots               — undo as an assignment
//    Part 9  The shell               — one actor owns everything
//    Part 10 Tests                   — surgical substitution, literal setup
//
//  SYNTAX FRONTIER — this file uses Swift 6.3/6.4 features whose spelling may
//  still shift before stabilization. Marked `// [6.4]` where they appear:
//    • `borrow { } mutate { }` accessors, and accessor requirements in
//      protocol declarations (the most speculative spelling here)
//    • ~Copyable associated types and generic parameters
//    • MutableRef, UniqueBox
//  Everything else is current Swift.
//
//  ═══════════════════════════════════════════════════════════════════════════
//  Part 1 — State slices
//  ═══════════════════════════════════════════════════════════════════════════
//
//  Each domain's state is a plain copyable struct. Copyability is the point:
//  it is what makes snapshots an assignment (Part 8). Slices use value-type
//  collections so copy-on-write prices a snapshot at O(changed slices).
//
//  Note that identity generation lives *inside* a slice (`TabsState.nextID`)
//  rather than coming from UUID(): state transitions stay deterministic, so a
//  command log replays bit-for-bit. The same motive puts the clock behind a
//  Link in Part 5.

struct Item: Hashable, Codable {
    struct ID: Hashable, Codable { let raw: Int }
    var name: String
    var price: Decimal              // in the till's currency
    var recipe: [Ingredient: Int]   // ingredient → units consumed per serving
}

enum Ingredient: String, Hashable, Codable {
    case beans, milk, oatMilk, syrup, cup
}

struct MenuState: Codable {
    var items: [Item.ID: Item] = [:]
}

struct Tab: Codable {
    struct ID: Hashable, Codable { let raw: Int }
    var customer: String
    var lines: [Item.ID] = []
    var settled: Bool = false
}

struct TabsState: Codable {
    var open: [Tab.ID: Tab] = [:]
    var nextID: Int = 1

    mutating func issueID() -> Tab.ID {
        defer { nextID += 1 }
        return Tab.ID(raw: nextID)
    }
}

struct StockState: Codable {
    var units: [Ingredient: Int] = [:]

    enum Shortage: Error { case insufficient(Ingredient) }

    mutating func consume(_ ingredient: Ingredient, count: Int) throws {
        let have = units[ingredient, default: 0]
        guard have >= count else { throw Shortage.insufficient(ingredient) }
        units[ingredient] = have - count
    }
}

struct Receipt: Codable {
    var tab: Tab.ID
    var total: Decimal
    var chargedAt: Date
    var reference: String           // gateway's charge reference
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 2 — Capabilities: access rights as protocols
//  ═══════════════════════════════════════════════════════════════════════════
//
//  A capability is one property, and its accessor list IS its meaning.
//  `borrow` is a read right: the witness must project storage in place — it
//  cannot legally hand back a stale copy. `mutate` is a write right, granted
//  by a *refining* protocol, so read-only code is read-only by type, not by
//  promise.
//
//  No associated types here: a state slice has one true type, and there is
//  nothing to substitute. Associated types are reserved for Part 5, where
//  substitution is real.
//
//  These protocols are ~Copyable because the environment that witnesses them
//  (the World, Part 7) is noncopyable — generic code must be able to range
//  over it.

protocol HasMenu: ~Copyable {
    var menu: MenuState { borrow }                       // [6.4]
}
protocol HasMutableMenu: HasMenu, ~Copyable {
    var menu: MenuState { borrow mutate }                // [6.4]
}

protocol HasTabs: ~Copyable {
    var tabs: TabsState { borrow }                       // [6.4]
}
protocol HasMutableTabs: HasTabs, ~Copyable {
    var tabs: TabsState { borrow mutate }                // [6.4]
}

protocol HasStock: ~Copyable {
    var stock: StockState { borrow }                     // [6.4]
}
protocol HasMutableStock: HasStock, ~Copyable {
    var stock: StockState { borrow mutate }              // [6.4]
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 3 — Reads: logic as constrained extensions
//  ═══════════════════════════════════════════════════════════════════════════
//
//  There are no service objects. `self` is the environment; the `where`
//  clause is the function's declared footprint. Everything below is
//  parametric — none of it can name the World, so none of it can touch a
//  slice it didn't declare. That parametricity is the access control.
//
//  Reads borrow, so any number of them may overlap and compose.

extension HasMenu where Self: ~Copyable {

    /// Sum of menu prices for a list of items. Reads: menu.
    func price(of lines: [Item.ID]) -> Decimal {
        lines.reduce(Decimal.zero) { total, id in
            total + (menu.items[id]?.price ?? 0)
        }
    }
}

extension HasMenu where Self: HasStock & ~Copyable {

    /// Can the kitchen actually make this item right now? Reads: menu, stock.
    func isAvailable(_ id: Item.ID) -> Bool {
        guard let item = menu.items[id] else { return false }
        return item.recipe.allSatisfy { ingredient, needed in
            stock.units[ingredient, default: 0] >= needed
        }
    }
}

extension HasTabs where Self: HasMenu & ~Copyable {

    /// A printable summary of one tab. Reads: tabs, menu.
    func summary(of id: Tab.ID) -> String {
        guard let tab = tabs.open[id] else { return "no such tab" }
        let names = tab.lines.compactMap { menu.items[$0]?.name }
        return "\(tab.customer): \(names.joined(separator: ", "))\(price(of: tab.lines))"
    }
}

//  A generic "feature" — what a view layer looks like under the parametric
//  perimeter. It borrows the environment for the duration of the call and is
//  provably unable to mutate anything: its constraints contain no `Mutable`.

func renderBoard<E: HasTabs & HasMenu & ~Copyable>(_ env: borrowing E) -> String {
    env.tabs.open.keys
        .sorted { $0.raw < $1.raw }
        .map { env.summary(of: $0) }
        .joined(separator: "\n")
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 4 — Commands: writes with typed footprints
//  ═══════════════════════════════════════════════════════════════════════════
//
//  A command is a `mutating` function constrained on *mutable* capabilities.
//  Read its constraint list as a compiler-checked effect declaration. The set
//  of writers of any slice is exactly the set of functions constrained on its
//  `HasMutable…` — a findable, finite set.

enum KioskError: Error {
    case unknownTab, unknownItem, alreadySettled
}

extension HasMutableTabs where Self: ~Copyable {

    /// Writes: tabs. Reads: nothing else.
    mutating func openTab(for customer: String) -> Tab.ID {
        let id = tabs.issueID()
        tabs.open[id] = Tab(customer: customer)
        return id
    }
}

extension HasMutableTabs where Self: HasMenu & HasStock & ~Copyable {

    /// Writes: tabs. Reads: menu, stock (to refuse unmakeable orders early).
    mutating func add(_ item: Item.ID, to tab: Tab.ID) throws {
        guard tabs.open[tab] != nil else { throw KioskError.unknownTab }
        guard isAvailable(item) else { throw KioskError.unknownItem }
        tabs.open[tab]!.lines.append(item)
    }
}

//  The big one. Note the internal structure every nontrivial command shares:
//  a read phase (borrows, freely composed across capabilities), then a write
//  phase that touches one slice at a time. `MutableRef` hoists the stock
//  access once and holds it across the loop — without it, every iteration
//  re-runs the accessor chain. It is non-escapable: the compiler proves the
//  hoisted access cannot outlive this function body.

extension HasMutableTabs
where Self: HasMutableStock & HasMenu & HasPayment & HasClock & HasJournal & ~Copyable {

    /// Writes: tabs, stock, payment session, journal. Reads: menu, clock.
    /// All of that is in the constraint list above — nothing is ambient.
    mutating func settle(_ id: Tab.ID) throws -> Receipt {

        // ── read phase ──────────────────────────────────────────────
        guard let tab = tabs.open[id] else { throw KioskError.unknownTab }
        guard !tab.settled            else { throw KioskError.alreadySettled }
        let total = price(of: tab.lines)
        let recipeRuns = tab.lines.compactMap { menu.items[$0]?.recipe }

        // ── write phase: stock, hoisted ─────────────────────────────
        var stockRef = MutableRef(&stock)                // [6.4]
        for recipe in recipeRuns {
            for (ingredient, count) in recipe {
                try stockRef.value.consume(ingredient, count: count)
            }
        }

        // ── write phase: payment (a stateful gateway — see Part 5) ──
        let reference = try payment.charge(total)

        // ── write phase: tabs, then the journal resource ────────────
        tabs.open[id]!.settled = true
        let receipt = Receipt(tab: id, total: total,
                              chargedAt: clock.now(), reference: reference)
        journal.append(.settled(receipt))
        return receipt
    }
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 5 — The substitution tier: Links
//  ═══════════════════════════════════════════════════════════════════════════
//
//  Tabs, stock, and menu have one implementation each — they got no Link and
//  no associated type. The payment gateway and the clock genuinely vary
//  (production / test), so here, and only here, the capability gains an
//  associated type and Link protocols select the witness.

protocol PaymentGateway: ~Copyable {
    mutating func charge(_ amount: Decimal) throws -> String
}

protocol HasPayment: ~Copyable {
    associatedtype Gateway: PaymentGateway & ~Copyable   // [6.4]
    var payment: Gateway { borrow mutate }               // [6.4]
}

//  The production gateway holds session state — nonces, an auth token — so
//  `charge` is honestly `mutating`, and the Link must demand a stored slice
//  from the World and *project* it. (The old pattern hid this mutation inside
//  a class; the access typing surfaces it.)

struct StripeSession: Codable {
    var authToken: String = ""
    var nonce: Int = 0
}

struct StripeGateway: PaymentGateway {
    var session: StripeSession
    mutating func charge(_ amount: Decimal) throws -> String {
        session.nonce += 1
        // ... synchronous wire call elided; async transport would live
        //     outside the World and re-enter as a command (see Part 9) ...
        return "ch_\(session.nonce)"
    }
}

protocol LinkStripePayment: HasPayment, ~Copyable {
    var stripeSession: StripeSession { get mutating set }   // demanded storage
}
extension LinkStripePayment {
    var payment: StripeGateway {
        borrow { StripeGateway(session: stripeSession) }     // [6.4] — see note
        mutate {                                             // [6.4]
            // Projection of a wrapper over a stored slice. Whether a mutate
            // accessor may yield a synthesized wrapper (with writeback) or
            // must project storage verbatim is the sharpest open spelling
            // question in this file; the fallback is to store StripeGateway
            // itself as the slice. The design is unaffected.
            &storedGatewayView
        }
    }
}

//  The mock pays attention to nothing and needs no storage — for trivially
//  copyable implementations, construct-on-access is harmless and the Link
//  demands nothing.

struct MockGateway: PaymentGateway {
    mutating func charge(_ amount: Decimal) throws -> String { "mock_ok" }
}
protocol LinkMockPayment: HasPayment, ~Copyable { }
extension LinkMockPayment {
    var payment: MockGateway {
        borrow { MockGateway() }                             // [6.4]
        mutate { &scratchMock }                              // [6.4] same caveat
    }
}

//  The clock: never call Date() in a command — replay dies. Production links
//  the system clock; tests link a fixed one.

protocol KioskClock { func now() -> Date }

protocol HasClock: ~Copyable {
    associatedtype C: KioskClock
    var clock: C { borrow }                                  // [6.4]
}

struct SystemClock: KioskClock { func now() -> Date { Date() } }
protocol LinkSystemClock: HasClock, ~Copyable { }
extension LinkSystemClock { var clock: SystemClock { SystemClock() } }

struct FixedClock: KioskClock {
    var frozen: Date
    func now() -> Date { frozen }
}
protocol LinkFixedClock: HasClock, ~Copyable {
    var frozenNow: Date { get }
}
extension LinkFixedClock { var clock: FixedClock { FixedClock(frozen: frozenNow) } }

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 6 — Resources: unique handles behind capabilities
//  ═══════════════════════════════════════════════════════════════════════════
//
//  The journal is an I/O handle: a duplicated one is a bug, so it is
//  noncopyable and lives in a UniqueBox. There is no database capability in
//  this app — the World is the truth; the journal of commands IS persistence.
//  Access is `mutate`, because appending to a log is a write and the typing
//  should say so.

struct WriteAheadLog: ~Copyable {
    enum Entry: Codable { case settled(Receipt) }
    private var fd: Int32   // owned file descriptor — the uniqueness motive
    init(path: String) { fd = 3 /* open(path, ...) elided */ }
    mutating func append(_ entry: Entry) { /* write + fsync elided */ }
    deinit { /* close(fd) */ }
}

protocol HasJournal: ~Copyable {
    var journal: WriteAheadLog { mutate }                    // [6.4]
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 7 — The World: the composition root
//  ═══════════════════════════════════════════════════════════════════════════
//
//  THE PERIMETER RULE: this is the only tier of the program allowed to name
//  `KioskWorld`. Everything above is parametric and therefore scoped; a
//  function that names the concrete World holds ambient authority over every
//  slice. In a real project, Parts 1–6 live in modules that cannot import
//  this one, making the rule a build error instead of a convention.
//
//  Reading the declaration top to bottom: the conformance list is the entire
//  production link map; the stored slices witness the capabilities directly
//  (a stored property satisfies a borrow/mutate requirement in place); the
//  UniqueBox makes the whole struct noncopyable — which is what forces
//  snapshots through the copyable slices in Part 8.

struct KioskWorld: ~Copyable,
                   HasMutableMenu, HasMutableTabs, HasMutableStock,
                   LinkStripePayment, LinkSystemClock, HasJournal {

    // State — copyable slices, the write-conflict map of the app
    var menu  = MenuState()
    var tabs  = TabsState()
    var stock = StockState()
    var stripeSession = StripeSession()          // demanded by LinkStripePayment

    // Resources — unique, noncopyable
    private var journalBox: UniqueBox<WriteAheadLog>          // [6.4]
    var journal: WriteAheadLog {
        borrow { journalBox.value }                           // [6.4]
        mutate { &journalBox.value }                          // [6.4]
    }

    init(journalPath: String) {
        journalBox = UniqueBox(WriteAheadLog(path: journalPath))
    }
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 8 — Snapshots: undo as an assignment
//  ═══════════════════════════════════════════════════════════════════════════
//
//  The slices are Codable plain data; the World cannot be copied. So "the
//  state of the app" is *necessarily* this struct — the type system closed
//  every other door. Undo, optimistic rollback, golden tests, and crash
//  payloads are all uses of the same four-line value.

struct KioskSnapshot: Codable, Equatable {
    var menu: MenuState
    var tabs: TabsState
    var stock: StockState
    // stripeSession deliberately excluded: gateway state is not domain truth,
    // and restoring it would desynchronize us from the processor. Snapshot
    // membership is a per-slice decision, made here, visibly.
}

extension KioskWorld {
    var snapshot: KioskSnapshot {
        KioskSnapshot(menu: menu, tabs: tabs, stock: stock)
    }
    mutating func restore(_ s: KioskSnapshot) {
        (menu, tabs, stock) = (s.menu, s.tabs, s.stock)
    }
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 9 — The shell: one actor owns the World
//  ═══════════════════════════════════════════════════════════════════════════
//
//  The World never leaves this actor, so it never needs to be Sendable, so
//  nothing inside it needs locks. Async work (real card networks, sync to a
//  server) runs OFF the actor, produces Sendable plain values, and re-enters
//  as a command — the World itself stays synchronous.

@MainActor @Observable
final class KioskShell {
    private(set) var world: KioskWorld
    private var undoStack: [KioskSnapshot] = []

    init(world: consuming KioskWorld) { self.world = world }

    func settle(_ tab: Tab.ID) {
        undoStack.append(world.snapshot)          // checkpoint: one assignment
        do { _ = try world.settle(tab) }
        catch {
            world.restore(undoStack.removeLast()) // rollback: one assignment
            // surface `error` to UI ...
        }
    }

    func undo() {
        guard let s = undoStack.popLast() else { return }
        world.restore(s)
    }

    // UI reads through borrow-constrained projections — e.g. a SwiftUI view
    // observing `Observations { shell.board }` — and is unable to mutate.
    var board: String { renderBoard(world) }
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Part 10 — Tests: substitution and literal state
//  ═══════════════════════════════════════════════════════════════════════════
//
//  The test world swaps exactly two Links — payment and clock — and supplies
//  no journal file. Everything else is the same code, re-verified complete by
//  the compiler. "Given" clauses are literals on slices, not mock setup.

import Testing

struct TestWorld: ~Copyable,
                  HasMutableMenu, HasMutableTabs, HasMutableStock,
                  LinkMockPayment, LinkFixedClock, HasJournal {
    var menu  = MenuState()
    var tabs  = TabsState()
    var stock = StockState()
    let frozenNow = Date(timeIntervalSince1970: 1_750_000_000)

    private var journalBox = UniqueBox(WriteAheadLog(path: "/dev/null"))  // [6.4]
    var journal: WriteAheadLog {
        borrow { journalBox.value }                                       // [6.4]
        mutate { &journalBox.value }                                      // [6.4]
    }
}

@Test func settlingDecrementsStockAndChargesOnce() throws {
    var world = TestWorld()
    let latte = Item.ID(raw: 1)
    world.menu.items[latte] = Item(name: "Latte", price: 5,
                                   recipe: [.beans: 1, .milk: 2, .cup: 1])
    world.stock.units = [.beans: 10, .milk: 10, .cup: 10]

    let tab = world.openTab(for: "Ada")
    try world.add(latte, to: tab)
    let receipt = try world.settle(tab)

    #expect(receipt.total == 5)
    #expect(receipt.reference == "mock_ok")
    #expect(receipt.chargedAt == world.frozenNow)       // clock is linked, not ambient
    #expect(world.stock.units[.milk] == 8)
    #expect(world.tabs.open[tab]?.settled == true)
}

@Test func shortageRollsBackCleanly() throws {
    var world = TestWorld()
    let latte = Item.ID(raw: 1)
    world.menu.items[latte] = Item(name: "Latte", price: 5, recipe: [.milk: 2])
    world.stock.units = [.milk: 2]

    let tab = world.openTab(for: "Grace")
    try world.add(latte, to: tab)

    let checkpoint = world.snapshot                     // given: a literal copy
    world.stock.units[.milk] = 1                        // sabotage after the add
    #expect(throws: StockState.Shortage.self) { try world.settle(tab) }

    world.restore(checkpoint)                           // rollback: one assignment
    #expect(world.snapshot == checkpoint)               // golden-state equality
}

//  ═══════════════════════════════════════════════════════════════════════════
//  Coda — what to notice on a second read
//  ═══════════════════════════════════════════════════════════════════════════
//
//  • Grep `HasMutableStock`: you have just enumerated every writer of stock
//    in the program. That query is the architecture.
//  • Parts 1–6 never mention KioskWorld. Delete Part 7 and they still
//    compile against TestWorld alone — the perimeter is real.
//  • The two test doubles required zero mocking framework: one protocol
//    conformance (MockGateway) and one literal (frozenNow).
//  • Nothing here allocates, locks, or dispatches dynamically once
//    specialized; `@specialized(where Self == KioskWorld)` on `settle` and
//    `@inline(always)` on the projections pin that down on hot paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment