Why we chose Elixir

Some time ago, I worked with a team to rebuild a company’s internal web application, which was based on a very outdated version of Symfony, and was no longer salvageable for several reasons.

After some time debating which technologies we should use, we decided to go with Elixir and Phoenix. In short, these tools gave us the productivity, stability, safety, and scalability (the company was planning on opening up the application to the public, with a new API added to the mix, so future performance was a bit of a concern) that seemed appropriate for the company’s plans.

A not-so-technical external consultant that was helping out with other parts of the business asked us to explain this choice, since he had never even heard of Elixir, so we wrote down a few of the reasons that led us to the decision, in a way that anyone with just a little bit of technical background could understand.

I haven’t been writing much here lately, and I thought it could make a good blog post for anyone else considering these tools. This was not edited or made more readable for the web, it is just a bullet-list dump of the reasons we wanted to convey at that time, so here you go.

It is hard to distill in a condensed version several months of conversations and research about a technology, ultimately leading up to the decision of using it for a project. I tried to summarise everything here but it’s still a long read.

Elixir has rock solid stability, which it gains from the BEAM VM (the Erlang underlying platform). Erlang was created by Ericsson for its telecommunications platforms, and to this date is still a very popular choices for mission critical software. Elixir inherits not only the BEAM VM’s stability, it also has all of Erlang’s OTP libraries and middleware available to it (in the same manner that languages that run on the Java VM usually have access to Java packages).

The fact that it is a functional language with immutable principles, along with its pattern matching capabilities and error handling mechanics, means that the resulting code is typically much simpler, easier to reason about, and more robust than the typical result from an object-oriented or imperative language. In turn, this means less bugs, easier maintenance, easier onboarding, etc.

Being a functional language also makes it less popular than other languages. But also because of that, it attracts more people who actually know how to write good software, instead of folks who did a 6-week “bootcamp” and were tricked into thinking they now know everything they need to be good software engineers. This also has an effect on the ecosystem around the language: the quality of Elixir packages is significantly higher than what is found in other more popular languages. In other words, even though there’s a smaller pool of people to hire from and libraries to use, the average quality is significantly higher. We don’t get as many low-quality developers, or libraries that are inneficient and riddled with bugs and security holes. On top of it, the Elixir community is well known for its friendliness and support, something we have experienced first-hand over the past year.

Elixir has amazing asynchronous and parallel processing capabilities (in its default configuration, the BEAM VM automatically uses all available CPU cores, so we get true parallelism, not just concurreny), granted by very lightweight processes (you can easily have millions of them running in a single simple machine) and associated functionalities that are very useful. These processes are completely isolated, share no variables / memory, and communicate with each other only through message passing. This allows for safe and easy concurrency and parallelism because there are no locks and no race conditions.

These processes are also meant to be discardable: “let it crash” is a guiding principle for writing good software, meaning that if something goes wrong, it should not fail silently while everything keeps running seemingly without a problem. The BEAM VM makes this super easy and transparent because it takes care of automatically restarting a process that terminates abnormally, while still generating logs, warnings, or whatever we need to detect the problem. In other words, Elixir is highly fault-tolerant by default.

An example of why these processes are useful: we don’t need separate infrastructure (message broker, task queue, task workers) to run asynchronous tasks as we would traditionally do, along with all the monitoring necessary to keep it running. Elixir’s processes and supervision trees make it very easy and very efficient to have background tasks, like what we would use cron jobs for, or task queues (for example, for sending emails, or periodically fetching data from third-party services). This is simple to do with Elixir alone and works just fine but if we want to go further, there is a package called Oban which can use the same database as our app (no external broker required), and implements a lot of functionality, like scheduled tasks similar to cron jobs, automatically retrying failed tasks with backoff algorithms, a dashboard with metrics and controls for the queues and tasks, and many other niceties.

The BEAM VM is also extremely efficient in computing resource usage, which means we spend less on infrastructure and, perhaps more importantly, reduce our environmental impact.

Though not as extensive as in other more popular languages, Elixir has many libraries available, the number of which grows by an order of magnitude or two when you consider OTP as well. You’d be surprised by how much is covered by the available packages. Due to not being one of the most popular languages, libraries for specific third-party services may not be available but we’ve found that for these, we usually only want a very limited subset of its functionality, and we can easily implement something ourselves. For example, there is no official library for accessing OpenAI’s API but the only thing their official libraries do is make a few HTTP requests. I would even argue that I’d rather not add yet another dependency to my projects if all it gives me is a way to make HTTP requests.

Elixir and Phoenix are known for low latency when handling requests, which is critical for providing good UX.

On top of that, Phoenix has built-in support for web sockets, which it uses in its Liveview library. It’s hard to describe how amazing Liveview is to someone who hasn’t used it yet. One of the best things about it is how easy it makes it to implement an application that behaves like an SPA without requiring the insanity of a full blown frontend framework. It is able to load data in the background (including loading data asynchronously after loading the basic structure of the page, a.k.a. hydration for a fast “first contentful paint”) and maintaining a bi-directional communication channel with the backend to modify data, get real-time notifications of any changes, and modify the DOM in a super efficient way (it’s so efficient that other frameworks are copying the methods used by Phoenix). This would allow us to create a web app that could be used as a PWA, saving us a ton of time and money until the need for a native mobile app would arise.

Phoenix uses a component-based approach for building web UIs, similar to what React and other popular frontend frameworks do, allowing for good code organisation and reuse.

Phoenix promotes a project structure that separates business logic from data access logic, and from web or API logic. As we talked about before, it is trivial to add API logic to a project that was started with only a web layer that renders HTML.

Both Elixir and Phoenix provide good telemetry and observability capabilities. Phoenix has a built-in dashboard that provides invaluable information and metrics about not only the application performance and health, but also the underlying BEAM VM.

Phoenix is well known for contributing significantly to productivity and developer happiness.

Bonus: This thread on Reddit is chock-full of goodness about Elixir and Phoenix.