Sendable vs non-Sendable. How should I model this?

Hello community!

I’ve been working on a simple package that tries to abstract Dependency Injection frameworks with a common API. I’m building it with approachable concurrency in mind, opting-in default isolation to MainActor, NonisolatedNonsendingByDefault and InferIsolatedConformances.

I defined these top level domain types:

/// A closure that produces new instances of a service type.
public typealias Factory<P> = () -> P

/// A type that describes a registration entry in a ``Container``
public typealias Registration<P> = (type: P.Type, factory: Factory<P>)

Everything works fine when I use them with Swinject:

public final class SwinjectContainer: Didi.Container {
    
    private let container: Swinject.Container

    /// ...

    public func register<P>(_ component: () -> Registration<P>) {
        let (service, factory) = component()
        container.register(service, factory: { _ in factory() }) // <- Using my `Factory` typealias
    }

    /// ...
}

but I’m having issues when using them with Factory:

public final class FactoryContainer: Didi.Container {
    
    private let container: any FactoryKit.Resolving

    /// ...

    public func register<P>(_ component: () -> Registration<P>) {
        let registration = component()
        _ = container.register(
            registration.type, 
            factory: { registration.factory() } // <- Capture of 'registration' with non-Sendable type 'Registration<P>' (aka '(type: P.Type, factory: () -> P)') in a '@Sendable' closure
        )
    }

    /// ...
    
}

That’s because Factory’s register function declares the factory closure as @Sendable:

public protocol Resolving: ManagedContainer {
    func register<T>(_ type: T.Type, factory: @escaping @Sendable () -> T) -> Factory<T>
    /// ...
}

I’m able to get rid of the error defining my Factory closure as follows:

/// A closure that produces new instances of a service type.
public typealias Factory<P> = @Sendable () -> P // <- Added `@Sendable`

From a modeling point of view, what should be the right thing to do?
Am I changing my package just beacuse a third-party framework uses a @Sendable closure? What are the trade-offs of making my Factory typealias as non-Sendable?

Any clarification/thought is appreciated!

Hey, :grin:

probably yes, you should add @Sendable.

You aren't just doing it for a third-party library; you are doing it to ensure your package is thread-safe by design in the Swift 6 era.

Why this is the "Right" Modeling Choice:

  • DI is Cross-Boundary: Service registration often happens on the MainActor (during app launch), but resolution can happen anywhere (background tasks, other actors). @Sendable guarantees this is safe.

  • Statelessness: A DI Factory should ideally be a pure "creator." If it needs to capture state, that state should be thread-safe (Sendable).

  • Consistency: Since you’ve opted into NonisolatedNonsendingByDefault, making your core typealiases @Sendable is the only way to make the library usable in strict concurrency environments.

The Trade-off:

  • Cons: Users can no longer capture non-Sendable types or mutable local variables in their registration closures.

  • Pros: The compiler will catch potential data races at build time rather than letting them crash the app at runtime.

Verdict: Add @Sendable. It moves your abstraction from "convenient but risky" to "strict and safe," which is exactly what a common DI API should be.

I hope this helps
~Divya

2 Likes

Why do you say adopting NonisolatedNonsendingByDefault implies need for @Sendable? I don't understand.

I linked them because under that flag, @Sendable acts as the 'passport' to let closures cross actor boundaries without being 'trapped' in their original region.I’m a student still learning, so feel free to correct me if I'm wrong!

@Sendable does that regardless of that language feature. NonisolatedNonsendingByDefault changes the language default from, when not otherwise isolated, callee determines isolation to caller determines isolation, making it simpler to reason about and allowing for fewer isolation hops.

2 Likes

Thank you all :grinning_face_with_smiling_eyes:

@DPrakashh I think this is what I was looking for, thank you!

1 Like