Skip to content

Concurrency

wiki edited this page Sep 4, 2026 · 1 revision

Concurrency

Container and Scope are both safe for concurrent use. Registration and resolution may run on different goroutines at the same time, and a single scope may be resolved from several goroutines at once.

Guarantees

Registration is safe at any time. There is no "wiring phase" the container enforces. Registering while another goroutine resolves is defined behaviour — the resolve either sees the new registration or does not, but the container's internal maps are never read and written concurrently.

Singletons are built exactly once. Concurrent first resolves of the same singleton produce one instance; the losers wait and receive it.

Scoped instances are built at most once per scope. If two goroutines race, the factory may run twice, but only one value is kept and stored. The redundant one is closed immediately, unless the factory returned the identical value.

Factories always run with the container's lock released. This is what lets a factory resolve its own dependencies without deadlocking. It is also why a scoped factory can race with itself as described above — the trade is deliberate: a factory that recurses is common, and a factory that runs twice under contention is cheap.

What is not guaranteed

A scope is not a synchronisation primitive. It protects its own bookkeeping, not the objects it hands out. Two goroutines resolving the same scoped *UnitOfWork get the same value, with no locking around it — if that value is not itself safe for concurrent use, the fan-out is a data race in your code, not in the container.

// Two goroutines, one shared *Tx. The container is fine; the transaction is not.
var tx *Tx
_ = ctx.Resolver().Resolve(&tx)
go func() { tx.Insert(a) }()
go func() { tx.Insert(b) }()

For per-goroutine values, use Transient — or give each goroutine its own scope.

Close racing a resolve is safe but not ordered. A resolve concurrent with Close either completes before it, or returns ErrScopeClosed and closes the value it built. It never hands back an orphan. But you cannot rely on which happens: shut down the work before closing the scope.

Ordering between registrations is not synchronised. If goroutine A registers *Database and goroutine B resolves it, B may see ErrNotRegistered. Register everything before starting the work that resolves it — in a rex application, that is what the declare-then-build lifecycle gives you for free.

Clone this wiki locally