Using an Elixir Umbrella

Using an Elixir Umbrella

Elixir Applications

In Elixir, an application is a component implementing some specific functionality. It is a unit that can be run independently, thus started and stopped. It can also be reused by other applications, much like a library.

When you create a new project in Elixir, the mix.exs file, which is responsible for the project's configuration, defines two public functions—application and project. The project section defines the application's name, the version of Elixir to be used, and makes a call to deps() that pulls in the necessary project dependencies.


def project do
		[
				app: :romanNumerals,
				version: "0.0.1",
				elixir: "~> 1.4",
				elixirc_paths: elixirc_paths(Mix.env),
				compilers: [:phoenix, :gettext] ++ Mix.compilers,
				build_embedded: Mix.env == :prod,
				start_permanent: Mix.env == :prod,
				deps: deps()
		]
end

Dependencies pulled down by mix are usually split into two types—libraries and OTP applications.

Libraries

Libraries provide functionality in a standard fashion where you use a module from the library to achieve a goal. An example of this is Poison. To include a library dependency, the module is declared in deps():

def deps do
 [{:poison, "~> 3.0"}]
end

OTP Applications

An OTP application is an application that often runs a supervision tree. This means there is a process monitoring it. An example of this is Plug. OTP applications have an Application file that starts the app and the workers.

For an OTP application the dependency is added in deps(), but additionally Mix has to be told to start the given app. Such dependencies are listed under the application function. For example, the extract below shows that the :plug application must be started in advance of the application you are creating:


def deps do
		[{:plug, "~> 2.1"}]
end

def application do
		[applications: [:plug]]
end

The examples above show dependencies that are external. These are typically pulled down from Hex, the Elixir package manager.

Internal dependencies are specific to the project you are writing. For example, you may be working on a web application that has a core engine specific to your business. Whilst this might be a separate project internally, it may be of no use to another company, or propriety code, so would not necessarily be published on Hex.

Elixir has two ways of managing internal dependencies. The first is using git repositories, where one project depends on another at a given git url:


def deps do
		[{:core_logic, git: "https://github.com/your_business/core_logic.git"}]
end

Let's take the example of a web-based Roman Numerals application. It has a front end—the view—and a back end that is responsible for translating decimal numbers into Roman numerals.

This could be modelled as two separate Elixir applications in two GitHub repositories. Imagine this project took off and the requirements changed such that it was necessary to support extra number conversions like converting decimals to binary and octal. Using the git repository approach, it would be necessary to create two more GitHub repositories and manage the dependencies with the front end. Over time, the more GitHub repositories that are added, the more difficult they can be to maintain versions and dependencies amongst the components.

The second approach offered by Elixir to manage projects made up of different components is the Umbrella applications.

Umbrella Applications

Umbrella applications allow you to create several sub-applications under one parent, which together make up an entire project. Each Umbrella application is in one GitHub repository, simplifying the code management.

To generate an Umbrella application, a special flag is passed to mix when creating a project:

mix new roman_numerals --umbrella

This generates a folder structure with an apps folder.

├── apps
├── config
│ └── config.exs
├── mix.exs
└── README.md

This structure is essentially a container. The apps folder is where all the sub-apps that make up your project will live. There is also a root level mix.exs where you can define application-wide dependencies.

To generate a sub-app, the usual command mix new sub_app is performed from within the apps directory.

Let's use the Roman Numerals web application as an example. It can be broken down into two components - A web front end and a backend component responsible for translating the numbers. This can be achieved by having a Phoenix sub application for the front end, which contains only the routing and presentation logic—such as controllers, views and templates, and a plain Elixir application for the back end that contains the logic for translating the decimal number to the Roman numeral.

Mix makes it easy to generate the sub-project structures:

  • cd apps

  • mix new roman_numeral_backend

  • mix phoenix.new roman_numeral_frontend --no-ecto

Mix generates the standard directory structure for each. Because the sub applications have been generated from within an Umbrella application, mix is clever enough to reference upward, thus pulling in any application-wide dependencies.

The resulting top-level structure would therefore be:

├── apps
│ └── roman_numeral_backend
│ └── roman_numeral_frontend
├── config
│ └── config.exs
├── mix.exs
└── README.md

Given that there is a web front end, requests will come into a router, and be processed by a controller.

Imagine the controller within roman_numeral_frontend needs to invoke the conversion module defined in roman_numeral_backend. For example:

RomanNumeralTranslator.convert(input)

The roman_numeral_frontend needs to have the roman_numeral_backend listed as a dependency so that it has access to the converter code.

This can be achieved by adding an extra dependency to the roman_numeral_frontend mix.exs.


defp deps do
		[
				# dependencies listed here...,
				{:roman_numeral_backend, in_umbrella: true}
		]
end

Now the front end app will have access to invoke the public methods from roman_numeral_backend app. Thus, the back end is essentially being used as a library.

Why take an Umbrella?

Umbrella applications help the developer build up a complex project that is composed of small, maintainable sub applications. This allows sub apps to be defined, implemented, and even run in isolation. This may allow teams to build out sub applications in parallel, provided that the interfaces between services are well defined. This keeps the sub applications decoupled from one another.

Despite implementing separate applications, it is easy to compile the code and run the tests for all sub applications with one command—mix test—from the root folder. This is because the mix.exs generated in each sub application references upward.


def project do
		[app: :roman_numeral_backend,
			version: "0.1.0",
			build_path: "../../_build",
			config_path: "../../config/config.exs",
			deps_path: "../../deps",
			lockfile: "../../mix.lock",
			elixir: "~> 1.4",
			deps: deps()]
end

This configuration states that all dependencies will be checked out to the parent's dep folder—in our case: roman_numerals/deps. The same mix.lock file is also used for all sub applications, which allows consistent versions of each library to be used when building the project.

Each sub app can have a distinct bounded context, a term often used within Domain Driven Design. Bounded contexts can be thought of as defining a distinct responsibility, and boundary around each sub app. This can help determine which logic will live within each sub app. This lends itself nicely to the Single Responsibility Principle. Each sub application can focus on one particular area.

Where web applications are concerned, Umbrella applications help keep the web layer as thin as possible. This allows for a separation between the UI and the business logic. This can help keep the amount of tests needed for the UI to a minimum, as most of the business logic will be tested elsewhere.

Is it Microservices?

Having several sub projects that make up an entire application can start to sound like microservices.

When building microservices, a communication protocol needs to be chosen so that each running service can talk with others. Often HTTP is the chosen protocol because most languages support it, and also there is flexibility in writing different services in different languages. Using HTTP can add latency, as requests have to be sent across a network and interpreted by the receiver.

As Elixir is built on top of distributed Erlang, it comes with the ability to access Erlang's Remote Procedure Call Services (RPC). This can act as a protocol amongst services, passing messages to specifically configured Erlang nodes, even if they are hosted on different servers.

Deployment can become complex with traditional microservices because separate services have to be deployed, monitored, and carefully versioned. Umbrella applications can remove some of this complexity, as they can actually be deployed as one bundle if required.

Use your Umbrella

The Elixir language provides a simple way of breaking a large application into several smaller ones. This means Umbrella applications do not need to be used from the start, but can be broken out later.

Splitting the entire application into separate sub applications lends itself nicely to separation of concerns. Each sub app has a distinct responsibility. Despite the child applications all being under one Umbrella project, Elixir provides lots of flexibility in terms of deployment. Umbrella projects can be deployed as a single entity, or the child applications can be distributed across several Erlang nodes.

There are drawbacks to consider. Dependencies in individual child applications bring in transient dependencies, which can result in conflicts with other sub apps under the Umbrella. Care has to be taken to keep versions consistent, or use the override flag when listing the dependencies to combat conflicts.


def deps do
		{:poison, "~> 3.1", override: true}
end

Circular dependencies are prohibited in Umbrella applications, so if this problem is seen, it's likely a sign that the breakdown of applications under the Umbrella has been too granular. It might be necessary to merge some.

Additionally, when more than one sub application is starting the same process, for example the ecto process for persistence, the ownership of the process needs to be shared so that the different child applications can access it and use it, particularly during tests.

To conclude, Umbrella applications take the overhead of managing several git repositories out of the equation, such as eliminating the coordination of pull requests between different git projects. They encourage weak coupling between components, making your overall application easier to test and maintain.