Multi-step Initialization in Elixir Applications

Andrew Combs
2 min readJun 14, 2021

--

When starting an Elixir application, it can sometimes be necessary to break up initialization into multiple steps. For instance, consider an issue tracker application where users can submit an issue that contains one or more tags. This may feel similar to the classic posts and tags example used when defining many-to-many relationships using Ecto, but we want our users to have a default set of tags available by default.

To do this, let’s first build out the required schemas:

Here we have two schemas that follow pretty simple conventions about many_to_many relationships. We can add issues and associate them with tags. This is exactly what we want!

To provide some default tag values, we’ll add something to the application configuration. We’ll provide these as a list of strings, where each string corresponds to a tag.name.

With the default names described in the application configuration, we can make sure they exist in the database when the application starts. Since this is application runtime behavior, it makes sense to put it in the application.ex file for the project.

However, we have a problem: The application starts IssueTracker.Repo as part of the supervision tree, which means we can’t actually interface with the database until after Application.start returns (because the IssueTracker.Repo process hasn’t started until the supervisor can initialize).

Thankfully, the OTP application behavior provides a way to perform initialization steps in multiple phases using the Application.start_phase/3 callback. From the documentation:

This function is called after start/2 finishes but before Application.start/2 returns. It will be called once for every start phase defined in the application's (and any included applications') specification, in the order they are listed in.

The application phases are defined using the start_phases keyword in the application function in your mix.exs file. The value is another keyword list containing <phase_name> : <phase_args> pairs. More information can be found here.

Now that we’ve defined a separate phase where our default values can be loaded, we can implement the actual phase in the IssueTracker.Application module. This is done by implementing the start_phase/3 callback, with the arguments (phase_name, phase_type, phase_args).

Now, whenever the application starts, it will attempt to write the initial set of tags into the database. The Ecto.Changeset.unique_constraint/2 call will ensure that duplicates are never written.

--

--