5
votes

How would one go about implementing a replaceable backend (or basically any part or module) so that it can be replaced at configuration/deploy time in Elixir?

My specific situation is a simple web app (in this case using Phoenix but I'm guessing this question applies to other situations as well) where I have a very simple backend using Agent to keep state but I see a need in the future for being able to switch out the backend more or less dynamically.

I'm guessing both Ecto and Logger do this to some degree but being new to Elixir it's hard to know where to look.

1

1 Answers

7
votes

This can be handled through an argument to the supervisor. For example, Ecto's backend supervisor takes an argument called adapter to specify which kind of database should be used:

# https://github.com/elixir-lang/ecto/blob/364d34bb135e2256fd48327464ada7f7fa2976f9/lib/ecto/repo/backend.ex#L13-L16

def start_link(repo, adapter) do
  # Start Ecto, depending on the supplied <repo> and <adapter>
end

You could do the same in your application, probably a single argument to start_link will be enough – let's call it backend

# my_app/lib/my_app/supervisor.ex

defmodule MyApp.Supervisor do
  def start_link(backend) do
    # use <backend> as you need to in here,
    # it will contain the module that is
    # specified in the configuration file.
  end
end

Now, you can of course set that argument dynamically when you spin up your application, based on a configuration file:

# my_app/lib/my_app.ex

defmodule MyApp do
  use Application

  def start(_type, _args) do
    MyApp.Supervisor.start_link(backend)
  end

  # get backend from configuration
  def backend do
    # ???
  end
end

Now, the only piece that is missing is how to get the backend from a configuration file. There's no single answer to that because there are multiple ways of doing this.

Mix Configuration

You can simply use the existing Mix configuration, but it has the downside that you need to recompile the application every time the configuration changes:

# my_app/config/config.exs

use Mix.Config
config :my_app, backend: MyApp.SpecificBackend

Then adjust your application to read in the specified backend:

# my_app/lib/my_app.ex

defmodule MyApp do
  use Application

  def start(_type, _args) do
    # same as above ...
  end

  def backend do
    Application.get_env(:my_app, :backend)
  end
end

Roll your own

You could also implement your own config file. I am not going into detail here, but this is the rough idea:

  • Keep a config file somewhere
  • Read an parse it in Elixir
  • Convert the string to a module name with String.to_existing_atom("Elixir.#{module_name}")
  • This will raise an error if the atom (thus the module name) does not exist
  • use it in your def backend function

Use an existing run-time configuration library

Basically a glorified version of the previous solution. Googling around a bit I found a library called Conform. It looks interesting but I can'T make any promises because I've never used it myself.