Moving the freelytics API from Golang to Elixir
You might have read https://maximilianehlers.com/open-analytics-a-new-approach/, where I talk about a recent project I started.
In my initial design I chose Elixir as the language that fits best. Via the BEAM VM and the Erlang OTP it can easily handle a massive amount of requests, where some will surely fail and it would nonetheless keep on running almost indefinitely. In addition I can live patch the running system, scale it up, inspect it etc. etc.. With the Elixir language on top of this, it is also fun to code and helps with a lot of the logic, mostly data related, I face in this project.
So how come I ended up with a Go codebase? It was all about time and feasability. My goal was to write the analytics platform and put it into production quick, to get feedback and to have a product that I can keep iterating on.
Elixir and especially the underlying Erlang technologies have a learning curve that collided with my goals.
After writing quite a bit of the logic in Elixir, I hit problems with understanding Supervision Trees, how the different configuration files in mix work together, and a lack of awareness of the general tools of the ecosystem.
So I switched to golang for V1 and it was fast since the language is so simple! I finished up the API in a day and was able to put the project online. And this is how it stayed for almost 3 weeks.
But in the back of my mind I knew that something in the codebase smelled bad and that eventualy I would have to revert to the technologies best suited for the application.
So I started reading more documentation on Elixir, especially on OTP, in the evenings after work, to wrap my head around the problems I last faced.
After doing this for a bit and with now more knowledge under my hat, I dared doing the rewrite this last weekend, taking a whole day aside for just coding.
It all worked out great and I learned a lot. There is still a lot of room for improvement but I am proud of the Elixir based API that is now running freelytics.
Here is a small breakdown of the challenges I faced when porting over to Elixir:
Plug is a great tool, that makes it super easy to add functionality to modules. While it is mostly as easy as just doing something as
use Plug.Router in your module, I encountered a hick up where I called plug functionality in the wrong order. This obviously lead to expected behaviour not showing up.
To be precise it was this Json Parser, that I originally called after the other plugs. This way, nothing seemed to get decoded and I went down the rabbit hole of debugging “unnecessarily” (I did learn in the end).
This is problem is probably easily avoided by reading the documentation more carefully.
Building a Dockerfile for production
This one was quite tough, but I luckily found https://github.com/Recruitee/mix_docker, which gave me some good directions.
The idea is to use multi-stage builds from docker together with distillery. This will end up creating a small image that only includes the necessary tools and the application, bundled as a standalone release.
This is the final Dockerfile for freelytics that I ended up with. Dockerhub can automatically build this.
The only thing that is missing for now is correct versioning.
Again I stumbled upon some missing mental models about OTP. But after rereading documentation such as Supervisors and Applications, with a concrete example to apply these concepts too I managed to wrap my head around it this time around, and hopefully implemented it all correctly.
The outcome being that I have 2 supervised applications running in the system:
- the database
- the router
This will help making the system scalable and healthy as more logic comes along.
Keeping the same data model
Unrelated to Elixir itself, but a concern I had, was modelling the data in the same way. In the golang implementation I used raw SQL queries, in Elixir I wanted to use a proven ORM, I chose ecto.
While ecto is opinionated on the data types it assigns when mapping between the Database and internal variables, it is possible to customize them by just using quoted type descriptions such as
I actually opted for the standard types provided in the end, just to make maintainablity over a long period easier. It is my experience that every customization to an opinionated base layer will increase maintainability cost.
This choice was only possible due to the early rewrite, where my schema was still easy enough to manipulate, that made it easy to migrate old data.
The next big thing was releasing. Since distillery packaged a standalone application release, there was no way to leverage mix for migrating the database. Luckily distillery allows the user to define release_tasks, which is described for migrations in this documentation page. Phew, this saved some work and speaks for the maturity the ecosystem has arrived at.
Migrating the data into the new system
Due to the choice of using standard ecto data types, the data migration now needed some manual intervention, which turned out to be rather straight forward.
docker exec -ti CONTAINER_NAME bash on the host to get into the database container, running
psql to interact with Postgres and voila:
All data is migrated.
Was it worth migrating? Most definitely.
- I learned a lot about Elixir and prototyping projects!
- Future extensions to the API will now be easier and safer
- The project can leverage the BeamVM and OTP, which is a massive gain and gives me confidence for maintaining the project
All the problems I faced initially were due to me not being prepared enough to just jump straight in. Elixir and Erlang take a deeper understanding of what is happening in a system and its applications. Once the knowledge is present though, it becomes possible to design incredible systems, with the work of a lot of smart people under your belt that code in the same design.
So, is golang terrible, and I needed to switch? No, definitely not. Golang allowed me to jumpstart the project, put it online and then quickly reiterate on it.
It all comes down to priorities, and mine shifted from trying out an idea, to building something maintainable and future proof, and here I trust Elixir + Erlang and the design decisions made before my time.
While Golang can surely be used for the same purpose, it will require tremendously more work, and I do not feel like reinventing the wheel. At least not on this project :)