Six months of Elixir
by Paweł Świątkowski
31 May 2021
Almost exactly six months ago I started to work for Fresha, where Elixir is a main language for backend. This happend after almost exactly eight years of working as a Ruby developer. There were may reasons why I decided to quit Ruby and seek something else but I’m not going to dive into it in this post. Instead, I want to share some observations after the technology switch.
As for my background, I did know Elixir before from some hobby projects. I also knew some Erlang from even older times. But my only professinal experience was Ruby or fullstack Ruby (combined with different flavours of JS on the frontend).
The switch from interpreted to compiled language is more difficult than I thought
Of course, I knew that Elixir is a different language and not just a dialect of Ruby. After all it’s much more functional, it takes immutability very seriously, it’s not at all object-oriented (in Java/classes meaning; in Alan Kay’s terms it’s actually much more object-oriented). But I think the most difficult mental shift was that it’s compiled. It affects the daily chores in many more ways that I could predict. For example:
- Switching between git branches, especially between a fresh one and a stale one, often forces the whole project recompilation. And with large codebases it takes some time.
- In CI, even if your tests are quite fast, you still spend most of the time on compilation step. Reducing this requires some tricks, such as setting up CircleCI caching or creating a custom base Docker image that has some things already precompiled.
- The cost of adding a dependency is completely different. In Ruby it affects the runtime, causing slower boot and larger memory fotprint. In Elixir the runtime cost is usually relatively small but it adds to the compilation time, often a lot, making your builds longer.
You’re gonna miss some things
I never realized how much I grew accustomed to dependency injection in Ruby until I switched to Elixir, where it’s rather hard to do. Ability to swap some related components in tests, for example, is really cool. In Elixir you need to either do it on top application config level or find out other ways to decouple your code. Or sometimes you just need to accept that it’s coupled.
Same goes for mocking. I used to think that an average Ruby codebase overuses mocks and stubs. And I still do. But there are situations when this is really handy - for example stubbing you random numbers generator for deterministic test results or even time (for time travel in tests) - these things do not come included with Elixir. You either need to introduce a layer of abstraction solely for the purpose of testing and mock it explicitly, or you have to give up on it and test differently (for example, only boundaries).
“Let it crash” does not fit most of the tooling
Erlang’s “let it crash” philosophy is all about understanding that crash under a supervisor is nothing wrong. In fact, it is often right. But that pholosophy is not very well supported by a lot of tools.
As an example, we use KafkaEx library for Kafka consumers. It embraces “let it crash” and upon any unexpected event, such as network hiccup or kafka repartitioning, when it cannot just proceed, it crashes and let’s the supervisor recreate the process, reconnect and start over. It is pretty smart because saves you from a lot of hard-to-test and possibly buggy code handling these kind of situations and trying to recover. BUT. As a side effect of that you receive alerts from Sentry or Bugsnag (or something similar), alarms are sometimes triggered and generally you see some redness in the logs.
These crashes are also hard to specifically catch and silence - you’d risk catching too much and swallowing real errors. On the other hand, unfortunately you soon start to get used to these kind of alerts and ignore them, which also brings the danger of ignoring important ones.
Yet to unleash the full power
I had most fun with Elixir and Erlang while building thing with multiple processes talking to each other and coördinating work. Sure, webdev with Phoenix is nice and in many ways better organized than in a standard Rails approach. But I still sometimes feel like I’m redoing Rails in a different language, while not using a full power that BEAM and OTP bring.
There is still a lot to learn there for me: how to use the Elixir abstractions like Agent or even simple GenServer in a daily work of a web developer. I had a few ideas I exercised, for example keeping static configuration data (kept in YAML) as a process alongside Phoenix app, which can be queried from the main app, but I’m sure I can do more.