Choosing Elixir

Bobby Juncosa
Edgewise Engineering
9 min readMay 5, 2018

--

Up till April 2017, Elixir was just one of many languages on my peripheral. Fast forward a year later, and Elixir is now our primary backend technology choice. We didn’t just tack it on as a sandboxed microservice; we refactored our core API to Elixir. Anyone who has embarked on such a journey knows this is no minor task; both from a technical standpoint, but also a business standpoint (time spent refactoring is time not spent writing new functionality).

Quick backstory

Edgewise started as a headless CMS, in Drupal 7, with the frontend built in Backbone.js. It was a very practical solution for an unfunded start-up. As we started to out-grow the “CMS as a web-app” paradigm, I started looking for something we could move to without too much pain. Symfony was that choice. Being PHP based, a good amount of the work was copy/paste into the Symfony structure. Converting Drupal entities to Symfony entities via the Doctrine ORM was relatively painless. The database was considerably cleaner, and the code was less fragile. Good, back to writing new features.

A little time passed, and Backbone.js was looking pretty long in the tooth compared to Vue, React, and Angular. So began the UI refactor from Backbone.js to Vue.js.

A little more time passed, and along comes this thing called GraphQL. Man, that was an easy decision to make. The benefits were immediately clear. And so, we refactored the REST API to GraphQL (still using Symfony).

Future proofing

By mid-2017, we were running a Symfony, Vue, and GraphQL monolith. Not bad, but not without a few pain points. We had a few initiatives slated in 2018 that were going to require all hands on deck, leaving a window of opportunity in Q4 if we wanted go big on a tech change. Doing so would set us up for 2018 to ensure that when we hit the gas pedal, nothing would hold us back.

For the record, I wasn’t particularly unhappy with Symfony, but PHP is just in another tier all together compared to now practical alternatives. Plus, Symfony was releasing Symfony 4, so even sticking with Symfony was going to require at least some work.

Now, I don’t have the luxury of spending months building prototypes in 12 different languages and pitting them against each other. I can’t ask the Skunk Works team what they’ve been working in and what they recommend. I can’t compare the pros and cons of all the different microservices we have in production. I’ve never been one to just blindly do whatever [insert Silicon Valley company] is doing. No, this was largely an exercise in intuition, and thoughtful R&D. Thankfully, I’ve learned to value my intuition at this point in my career.

Narrowing it down

I originally cast a wide net, being careful to stay open minded and not let familiarity bias the decision too much (although, familiarity certainly is valuable and can provide nuanced context that you may not get out of prototype projects).

Pretty quickly, I had narrowed it down to Node.js, Go, and Elixir. High level, notable value-props for me were:

Node: take advantage of the current upward trend (easy to find devs who want to work in the tech), extremely active community, the ability to leverage “Universal JS” (aka “Isomorphic JS” or SSR), sockets / real-time, and improved performance over PHP.

Go: another upward trend, albeit less mainstream than Node. Smaller community. Amazing performance. Terse syntax. Pragmatic quasi-OOP style.

Elixir: probably the least mainstream of the three, and I had limited experience working in functional languages. Still, devs working in Elixir are evangelists (in a good way, not a creepy dogmatic cult way). Excellent performance. Extremely terse and clean syntax, excellent documentation, welcoming community, and built-in sockets. Notice, I’m leaving out the canned responses of “concurrency, fault tolerance, immutability, etc.” (I’ll touch on that).

Sucker for syntax

I can’t stand noisy syntax (eg. unnecessary curly braces, semicolons, return statements, etc.). Sure, I was aware of the low-level reasons why Erlang (and by extension Elixir) were so beloved. But for me, it wasn’t fault tolerance, self-healing, hot code swaps, a gazillion simultaneous socket connections, or nine nines that got me excited; it was the functional syntax. It’s the relationship between syntax and immutability that makes Elixir truly a powerful and productive language (IMO). Here are a few stand-out language features worth mentioning (apologies if I’m preaching to the converted):

Pattern matching

Basic x = 1 pattern matching examples don’t really due the feature justice (hint: it’s not an assignment operator). Things really start rocking when you start matching more complex patterns in functions and case statements.

For example, let’s say you have a parent function with a conditional statement that forks the logic of that function into two branches. You might then move the branches into their own functions (eg. for testability). You now have three functions, with the parent function determining which of the child functions get called. The two child functions have different names, naturally.

With Elixir, you wouldn’t need the parent function. The child functions can even have the same name (imagine that!). The arguments of the child functions are pattern matched to determine which one gets called. Boom! Like that, you have one less function, less code (ie. less chances for errors), and a more intuitive “grouping” of the child functions. You even get quasi-regex and object destructuring as part of the pattern match.

Pipeline operator

This is kind of like currying and chaining; except much cleaner, consistent, doesn’t require any external dependencies, and doesn’t mutate the object. The return value of the called function is fed into the first argument of the following function.

slug =
user
|> User.full_name()
|> IO.inspect # Peter Parker
|> String.downcase()
|> IO.inspect # peter parker
|> String.replace(" ", "-")
|> IO.inspect # peter-parker

Notice the IO.inspect between each step. Very useful for debugging.

Worth noting here, String.downcase() does not mutate the user object; it simply takes a string of characters, downcases them, and returns the value (you don’t return this). These function are not aware of the original user variable. If you were to inspect the user variable at the end of this pipeline, it would be exactly the same as it was before we ran through the slug pipeline. You don’t need to memorize the times where the function mutates the variable, and the times where a new value is returned. Elixir is consistently immutable. That’s not just one less thing to have to think about, that might mean hundreds of things you don’t have to think about.

Tuple responses

So simple, but so effective; especially when combined with pattern matching. Let’s say you need to make an API call, and the raw response comes back with a 401. In JS, it wouldn’t be uncommon to throw an error, catch said error, and then use an if/else if/else in the catch to know what the actual error code was. In Elixir though, you simply return a tuple, where the first element is an atom. It is common to use :ok for success, and :error for failure. So in this case, you might return {:error, error_response}. Instead of using a try/catch, you’d pattern match the response. Something like this:

case foo() do
{:ok, response} ->
# all good

{:error, %{code: 401}} ->
# unauthenticated logic
{:error, %{code: 403}} ->
# unauthorized logic
{:error, %{code: 404}} ->
# not found logic
{:error, error_response} ->
# generic error logic
end

Not only is this much cleaner in my opinion, but it’s also optimistic. I can put the success logic at the top (happy path), followed by common errors, then edge cases, then finally a generic error handler.

Making a decision

While having little experience in a full-fledged functional language, Elixir came very naturally. Even though this was somewhat uncharted territory for me, the initial experience with the language, documentation, and community were all very positive. With that, I made the decision that Elixir beat Go as the emerging high-performance option (technically, we’re using Go for lower-level operations like image processing, but that’s for another time). So the next battle was Elixir vs JS.

Full disclosure, I don’t really like working in JS. There are quite a few criticisms I have for the language, and I don’t really go looking for excuses to use it. I think my distaste for JS goes as far back as ~2008 when we had the awesome ActionScript 3 in Flash, while the ECMAScript community was bickering over ECMAScript 4 (despite Adobe’s best efforts). By my estimate, JS is probably 9-ish years behind where it should be. Still, of the three (Elixir, Go, and JS), JS is the one I have the most experience in, so I was certainly cautious not to let the real-world experience of JS jade me, and not just look at Go and Elixir with rose-tinted glasses.

Ironically (masochistically?), the JS community seems to be increasingly fascinated (and inevitably dogmatic) with functional programming. Many are trying to shoehorn functional programming into JS wherever they can. But wait, Elixir is already functional / immutable! Babel 7 has the pipeline operator (along with a few other things borrowed from functional languages), but it just feels like JS is trying to play “me too”, as opposed to leading.

Community wise, I find the JS community to be very provincial, and often condescending (not you of course, you’re great). You can’t even submit a bug report to GitHub without jumping through hoops (specific formatting, providing your blood type and what you were eating at the time of the error, 3rd party account registrations, etc.). God help the newbies that post a question in GitHub instead of using the official forum / Gitter / Slack / Stack Overflow / Discord (you better know which one is for what, idiot!). The Elixir community on the other hand, is very open and welcoming. It’s not uncommon to see José Valim or Chris McCord answer Elixir 101 questions without an ounce of condescension, and even humbly recognize that the question just might be an indication that the docs need improvement. So all that combined, I decided to dig into Elixir, and leave JS in the browser (for the most part, but I’ll leave the topic of SSR for another time).

To Phoenix, or not to Phoenix

So the language decision was made, Elixir it is. The next decision was whether or not to use the Phoenix framework. Admittedly, this was a short decision. I learned Elixir by first reading Programming Phoenix. In my opinion, Phoenix fits right in the Goldilocks Zone for framework sizes — not too small that you need to add 87 dependencies to really build anything of practical value, and certainly not bloated with a bunch of overly opinionated paradigms and complex dependencies (Symfony is a bit in this camp, IMO). The decision to even use a framework at all can be a controversial subject, so I’ll leave it at that so we don’t go tangent.

Anyway, add in Guardian for JWT auth and the excellent Absinthe library for GraphQL, and we were off to the races.

Refactoring

Despite having a pre-existing database in MySQL (Phoenix defaults to Postgres, but works just fine with MySQL), we were able to relatively painlessly port the domain and API over to Phoenix (largely a credit to Doctrine and Ecto being very good database wrappers). It was probably easier than going from Drupal to Symfony. It also gave us a good excuse to take a step back and really look at how we were structuring data, naming conventions, etc.. Even little things like converting legacy unix timestamps to datetime, and renaming created / updated columns to inserted_at / updated_at.

Refactoring is kind of like moving out of a house you’ve lived in for a while. It’s kind of a pain the ass, but it’s also a good time to purge the junk you no longer need, and have a clean start in your new home.

The refactor and feature development happened in parallel, all the way up till the last month; when we finally put a feature freeze on Symfony development.

Moving further towards SOA

We’re still using Symfony for some things, namely cron jobs and our image reverse proxy. Those aren’t huge priorities to refactor, for the time being. We’ve also split out the presentation layer entirely, to Nuxt.js. Phoenix is just the API. So our presentation layer, Symfony services, and Phoenix API all deploy separately via Docker containers to EC2.

Conclusion

So far, I don’t have a single regret about going with Elixir and Phoenix. Our code is cleaner, more intuitive, and far more performant. It feels like we’re using the right tools for the job, and we’ve got plenty of headroom to grow into. I can see Elixir being the backbone of the Edgewise tech stack for years to come. It’s a bit of a drag to refactor, but the juice was worth the squeeze.

I’m bullish on Elixir, and I hope to see more companies adopt it. If you find yourself evaluating server-side technologies, Elixir is certainly worth serious consideration!

--

--