How to Control the World
Brandon Williams and Stephen Celis (2018, via Christian Tietze):
While unconventional, we hope that it’s obvious that this solution of controlling dependencies is superior to the traditional solutions in use today. It also gives us an opportunity to reevaluate deep-seated beliefs we may have. We should continuously question our assumptions. In this case, we found that:
Singletons can be good (as long as we have a means to control them) and global mutation can be good (when it’s limited to development and testing). Blanket statements against singletons and global mutation are fun to make, but we were able to find real value in using them.
Protocols aren’t necessarily a good choice to control dependencies. Protocol-oriented programming is all too easy to reach for when a simple value type requires less work.
The global mutable struct approach certainly reduces boilerplate code, but I’ve never understood how it can work with threads. Most basically, how do you deal with modifying the dependencies for multiple unit tests that are running concurrently?
Their newer Dependencies library handles this more directly:
A dependency management library inspired by SwiftUI’s “environment.”
[…]
For example, you can easily control these dependencies in tests. If you want to test the logic inside the
addButtonTappedmethod, you can use thewithDependenciesfunction to override any dependencies for the scope of one single test.
The dependencies are stored in a TaskLocal. But it still feels like a partial solution because not all code uses Swift Concurrency, and you still need to worry about propagating dependencies:
It is important to note that task locals are not inherited in all escaping contexts. It does work for
Task.initandTaskGroup.addTask, which make use of escaping closures, but only because the standard library special cases those tools to inherit task locals (seecopyTaskLocalsin this code).But generally speaking, task local overrides are lost when crossing escaping boundaries.
[…]
In order to access dependencies across escaping closures, e.g. in a callback or Combine operator, you must do additional work to “escape” the dependencies so that they can be passed into the closure.
It’s more ergonomic not to have to propagate dependencies explicitly, but relying on implicit behavior can be harder to understand and error-prone.
Previously:
- Canopy
- Lightweight Dependency Injection Using Async Functions
- Using Swift Protocol Composition for Dependency Injection
- Mocking Dependencies With Generics
- Hypo Dependency Injection Framework
- Typhoon Dependency Injection Framework
- Dependency Injection Is a Virtue
- Dependency Injection Myth: Reference Passing
Update (2026-05-29): Adam Zethraeus (Mastodon, via Matt Massicotte):
I recently had the misfortune of having to retrofit a lifecycle into an app built with
swift-dependencies— and this post is intended as both my retrospective and a PSA.[…]
The marketing promises “SwiftUI’s environment for everything”; the runtime ships a global
@TaskLocallookup with a hidden cache and no scope ownership, and the maintainers have confirmed on the record that the gaps you are about to hit are not bugs.Use it for small systems like an app with a handful of globally scoped services. That is the configuration it was designed for, and it wins there. Elsewhere, the cost-to-correctness ratio is poor.
I want to start by addressing each of your remaining “footguns” directly (you seem to have removed one already by the time I saw your post). Each has at least something that we think should be corrected.
[…]
It seems your article can be summarized as “Dependencies made choices I don’t agree with, and therefore it is full of footguns.” A better approach to this kind of article might be “How I would design a dependency injection library.” Share some actual concrete ideas, with compiling code to make it compelling, and then compare your ideas to ours. In the course of doing so, I hope you will see that there is no silver bullet; instead, it’s a minefield of tradeoffs.
3 Comments RSS · Twitter · Mastodon
I'm not an eminence on this but when I built up https://github.com/Gabardone/GlobalDependencies I figured it was ok to inject (with a default) since it makes most of those concerns moot.
If you end up having a nightmare dealing with the injections then the problem is usually not with whatever dependency injection logic you're using but rather with your dependencies themselves having gotten out of hand.
> The global mutable struct approach certainly reduces boilerplate code, but I’ve never understood how it can work with threads. Most basically, how do you deal with modifying the dependencies for multiple unit tests that are running concurrently?
Concurrent unit tests are spun up on their own processes, so they all have their own, independent, global stores. The main issue with the global mutable approach is if you decide to mutate your "world" outside of tests, _e.g._ in the lifetime of your production application, but we definitely don't recommend that.
> It’s more ergonomic not to have to propagate dependencies explicitly, but relying on implicit behavior can be harder to understand and error-prone.
This is for sure fair, but we think it's only generally a concern when you override dependencies in certain branches of your application. If you override dependencies for a certain flow (_e.g._ an onboarding flow, a demo, etc.), then propagation needs to be explicit if you also want to unit test that flow. But for simpler setups, where you simply need to override dependencies in your unit tests, taking extra steps for propagation are not necessary.
@Stephen Ah, you are right about the separate processes. I must have been misremembering some other issue that I once had. Thanks!