I’ve been bumping into some rough edges recently with Elixir dependencies that use app config and start their own supervision trees and I want to petition the community to move towards better library design
Thanks! finally I understood the critics I’ve been receiving in my library!
Sometimes we want to take control over the supervision tree, sometimes we just expect the library to do its work once added to mix. Sometimes the configuration is an anti-pattern, in 99% of cases it’s not, specifically in modern environment when it’s all containerized. Also, starting an application is necessary if it needs to track the main application startup.
I finally came up to a compromise. I declare the application in my libraries, empty-handed by default. Then I got an option to have a config and I use it to start the supervision tree within the application.
100% with you in this uphill battle. Unfortunately, the DX of letting the app run its own supervision tree is just so good and Req is indeed a good example. Same goes for accessing application configuration: it’s just so much easier to say “put an api key into you config config :foo_client, api_key: <my api key> and you’re ready to go!”
I agree with the post for the most part, but I would push this idea further: just don’t use applications, let users start their own trees. To simplify this, provide a single entry-point so that starting your dependency is a single line of code.
I can see comments here and I’ve heard lots of them in the past saying “its better DX”, but it is actually not. If you think about it, the offer of applications is as follows: You get global state by default (which is not very easy to disable) and in return you don’t have to write a single line of code which adds the process in the supervision tree. Global state is bad, it creates problems, and fix for this is just one line of code
And DX is actually worse. Consider Ecto. I can’t just connect to different databases in runtime, because database connections pool is tied to the Repo module (the one which calls use Ecto.Repo in it), and Repo module is required because Ecto uses global application state in order to set up pools of connections. That’s why you dont start the {Ecto.Repo, connection_opts} in the supervision tree, and you start {MyApp.Repo, ....}. That leads to funny problems like you now have to write a separate module if you want a separate pool of connections for, for example, read-only replica or different prefix or isolated test database. A huge set of problems caused by this Application approach is fixed in many ways by many libraries and approaches, from sandboxes with ownership, Ecto.Multi which accept repo in a callback to libraries like triplex which handle multi-tenancy.
And it is true even for smaller libraries. In the early days of Elixir, some libraries were requiring their own configuration formats, they’d have Application implemented just in order to parse some .env file, pull out the variables and populate the application env (or application configuration as it is called in the post above). Library Number is a good example. It doesn’t have the Application, but it uses application env where it could just use options.
However there is only one thing which applications do good: topological sorting of the dependencies. If I have a dependency A which uses dependency Band there is another dependency C which uses dependency A, the application logic will perform topological sort to make sure that dependencies are always started in the order B, A, C.
But this thing is extremely easy to implement and libraries like Parent by Sasa Juric showed that it is possible and fairly easy, definitely not a rocket science.
I agree.
I love how OTP primitives make complex state management explicit and easy to follow without dictating how the project needs to be structured. Dependencies starting their own supervision trees could be more convenient, but as soon as I need to debug something or want to implement a usage pattern the library designer didn’t foresee, things could get a lot less convenient. I can understand the desire to make a library super easy to use for the “common” cases, but that usually comes at the expense of making it more cumbersome to use in other scenarios. And most real-world problems seem to fall into the “other” category in my experience.
I also prefer it when I can put config into the initial state in the child spec as opposed to having a 10k line config.exs file. It keeps the process orchestration and configuration in one place. And if I did want to have the dependency’s config in the config.exs, I could still add a config key for it myself and just call Application.fetch_env!/2 in the child spec. ![]()
That is not true. You can 100% connect to different DBs using the same module. It is not common practice, because that is not the common problem, but it is fully supported and documented.
MyApp.Repo.start_link(
name: :some_client,
hostname: "client.example.com",
username: "...",
password: "...",
pool_size: 1
)
It is not fully ergonomic, I agree, but at the same time that is not a common situation when you need such thing.
About the topic, I mostly agree with @jola. There are however grey areas sometimes. If you use Application just for env, then yeah, it is dumb, but there are situations when there is no point in having multiple instances of the processes provided by the library. Like in my systemd library, where there is no point in having multiple instances, and moving that to the user-side to manage processes for this would only make it worse wrt. UX.
Another thing is that sometimes the process running in the background should be transparent to the user. Like in my aww library. Some may argue, that cache there is a wrong approach and user should cache on their own. But at the same time, when you are using (most of the time indirectly) Erlang’s DNS resolver do you think that there is a cache for A and AAAA responses? Because there is one.
Another thing that uses their own supervision tree, but people do not think about it at all, is Telemetry. Yeah, really, there is supervision tree. To be fair, it would be impossible to implement without that tree (at least if we want to use ETS, after you fire :telemetry.persist() it is not needed anymore).
So I would say that it is a complex topic, but when in doubt - do not spawn your own supervision tree, you ain’t gonna need one probably.
Yeah, I’ve missed that, it was implemented at around 3.3.0 and became stable (as in without “It may be removed in future releases” note) at around 3.8.0. That’s why I never used it, apparently. But now I know, thanks!
Which is a good point, but people don’t use it not because “it is not a common situation”, but because it is hard to use and it is a bad DX. Situation, however, is very common, and it is testing when I want to check how multiple processes use the database. If I had the manual startup approach with explicit pool, I’d have something like this
- I create a prefix of the database, or I set up a sandbox pool which executes every query in a single transaction.
- I start the database connection pool by some name.
- I start the subset of my supervision tree I want to test.
- I pass the pool in start options.
- I perform the test and it is fully async and isolated because it uses a pool which is separate from other tests.
This way I dont have to write any testing code, because pool propagation is already there. I don’t have to rewrite the code in order to make it work with a different database. Plus, I can now see which functions work with database, because I pass the pool around explicitly.
And also, at any moment, I can make this explicit pool passing approach into implicit one, like Ecto does today, by introducing one wrapper module which would take around 30 lines of code.
I do agree that that direction is more natural, but you can also do it the other way round. Wrap the repo functions with calls doing put_dynamic_repo. You can already start an ecto repo with a different name, which includes the connection pool. I do all that in a project of mine, where I deal with catalogs provided as sqlite dbs. I agree that the apis and ectos process tree are not optimal due to legacy reasons, but ecto added all the pieces needed to deal with dynamically run repos as well.
A few years ago I wrote an article on that(and I just discovered that my abandoned blog is down bc the domain expired
, i’ll send the link here if i’m able to get the old domain set or if i setup a new domain), I was way less polite and ranted a lot about the subject.
Supervision trees are not for DX, they’re for resiliency, DX is your libarary with good defaults so just a plain copy/paste of your docs resolve 90% of the needs someone have.
It feels really terrible when a library hijacks the power to define how your app starts and recovers, and supervision tree is about that, not about ease of use.
Again, needing just one instance of a supervision tree is not the issue, it’s aboult controlling how it gonna behave when it fails. If you just need one instance, make your childspec use a name scoped by your library, or allow it to optionally receive the name and default to getting the name from the application env that the user can set, the dev using it decides where it sits in their supervision tree, it doesn’t lose usability, it doesn’t hijack the control over what should bring the app down or not.
Not to the quoted people here, but people writing libraries need to remember once an application that your app depends doesn’t start it blocks the entire application of starting, I’ve had a lot of issues just because one particular library couldn’t start and it was impossible to start a new instance of the app no matter how I configured their library, I’d need to remove them of my dependencies to make it work.
For writing applications instead of libraries my rule of thumb is: can you assure there is no scenario where using your application as dependency will block the main app of starting? If you can’t assure that, create a worker and allow people to set their supervision tree appropriately to the place your library have in their systems.
Isn’t this just to a good degree a crutch for configurability and shared state problems?
I personally am lucky enough to only use true library dependencies, not being part of the elixir web dev world but the other… 5?.. percent or whatever, so these stacks and ecosystems people typically use do not impact me.
But then I create my own application stack and run into the same problems, how to efficiently share state and configuration around if needed, and often reinventing the wheel, I guess. Part of it is that there are so many options… do I want a full-fledged DB, a key-value in the application, or with ETS, or do I want to optimize for read-only values… what are my guarantees, my performance… ease of use vs. my own requirements. I guess some of this could benefit from a unified layer allowing you to switch between options and their guarantees. Maybe somebody already did that.
If an external application added to this mess, I would not be happy. That’s why I personally am somewhat biased against dependencies that are apps, not saying they have no place, but I’d rather have control over certain aspects of my system. I do badly enough on my own writing distributed systems…






















