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.






















