How do you approach adding or migrating to Elixir from a different language?

To clarify the question, I would like to discuss the technical aspects of your approach to introducing Elixir in the context of a production system running (maybe poorly) in another set of languages or technologies. I understand that there are possibly more important social aspects to this question, but I’d like to focus on the technical aspects. I’m hoping not to discuss in abstract, but instead with concrete experiences migrating to or adopting Elixir.

I know that the decision to switch is a social AND technical decision, and I realize sometimes the answer is “let’s not do that”. But let’s take it as a black-box dependency. Once you have made that decision: In your experience, what are your short-term, medium-term, and long-term technical plans for introducing Elixir? Do you aim to fully replace the other parts of the system? Do you run Elixir services alongside other services? Do you prefer an Elixir monolith even if the existing system is built with microservices? How do pieces communicate?

My specific context at work is a NodeJS monolith connecting to a Mongo cluster, plus a few cloud infrastructure appendages like an Azure Service Bus. I’m happy to hear about whatever context was present in your experience, as deemed relevant by you.

I’m also happy to be pointed to a relevant thread or post to read!

In short, I feel that there are many resources on why folks have adopted Elixir. But I haven’t found many on how they’ve adopted Elixir!

3 Likes

So I can only speak to how I did it, but I have successfully migrated a rails monolith to an elixir monolith app, over the period of about a year and a half. So our stack at the beginning is nginx serving a static frontend, a rails json API, a MySQL database, and all of those are in docker containers inside docker-compose. Nginx is configured so any requests to /api are proxied to rails. At the end, we ended up with nginx serving the frontend, a phoenix json api, raills API for some legacy accounts, and a postgres database. The way we decided to do this is by using the Terraform library. Requests to the api first hit nginx, are proxied to phoenix, and if phoenix doesn’t understand it then it’s sent to rails using the terraform library.

So the first steps was setting up terraform, making sure rails and phoenix had the correct database credentials, and then defining some basic ecto schemas. I started here by copying out of my schema.rb file and pasting it into the ecto schema file, then changing the syntax so it matches ecto’s, which worked pretty well. Then I needed to configure the JWT library Joken so that it matched the same secret key and settings we were using with devise-jwt on the rails side. Now we can log a user in via rails and authenticate that person in elixir land. Yay, now it’s time for feature development!

The business case behind adopting phoenix for me was channels, so it makes sense that the first feature we shipped was a chat system. Then it was a slow process of new feature development happening in elixir, and any time I needed to fix a bug with some ruby code I tried to rewrite it in elixir. This was by far the longest step of the whole process.

The two lingering things in rails for a long time was image uploading, which we were doing in rails with ActiveStorage, and user registration/authentication, which we were doing with devise. To solve the image uploading issue, I used Waffle and waffle_ecto to upload any new images, and I dug into the activestorage source code to figure out how they were generating urls for the images, and I replicated that in elixir code so we could generate a URL in elixir based on the activestorage tables that were stored in the database. This was probably the only actually hard code to write in this whole process. I have considered open sourcing this, but I’m not sure if anything has changed in activestorage since I wrote that code, and I wouldn’t really want to maintain it as it’s kinda a one and done thing in terms of usefulness. This was with rails 5.3 at the time. So once we had that, it was basically just writing a function to check if there is a waffle uploaded image, if so use that url, and if not then call the activestorage url function. For passwords, I did something similar except using the phx_gen_auth library to create the new password tables. When you change your password or create a new account, it uses the phx_gen_auth code to set that. When logging in, it first checks if your password is valid with phx_gen_auth, if that fails it calls rails and does the check with devise.

I also migrated mysql to postgres during this time, and while not strictly related I thought I’d share that story as well. Elixir made it super easy, I added both the mysql driver and the postgres driver to my dependencies, then I renamed my repo module which was set up to work with mysql to OldRepo. Then I created a new repo module and configured that to work with postgres. I then wrote a function to call from IEx, which basically listed all the tables I wanted copied, and basically did Repo.insert_all(OldRepo.get(table)), though I think I needed a bit more code than that due to the limits postgres has on the number of items you can insert at one time with insert_all. All in all, pretty easy!

Hopefully this is what you were looking for!

8 Likes

As @John-Goff pointed out, you absolutely need a solution to route to your old monolith and your gradually growing Elixir app on a per-route basis, and terraform is indeed one very good way to approach it. This is your very first stop.

Once you handle that successfully (try and just have a testing endpoint like /hello/test/elixir or some such as a start) then it’s a matter of just moving things endpoint by endpoint.

One key element is to keep the DB migrations in the old monolith for the time being and export a full SQL data structure after each migration is added and export that SQL file to the Elixir app so it knows the DB shape (although that’s strictly optional; as long as you define your Ecto shema modules well then it doesn’t really matter if you have a priv/repo/structure.sql file or not – this is important if you want to provision an empty DB while working only with the Elixir code).

Another key element is to keep the Elixir app on a separate server. Elixir (technically the BEAM VM) is by default tailored to use all CPU cores of the system so for best results keep it alone in a dedicated instance even if has like 2 cores and 256MB RAM.


  • SHORT TERM: Parallelism benefits: rewrite the parts of the old monolith that could benefit from heavy parallelism and/or are already struggling when a certain parallel load is going on. That should be your first priority! To make things better, the mere fact of using Ecto and Phoenix already gives you that automatically without you lifting a finger.

  • MID-TERM: Scaling. I got tired of trying to convince people that Ruby / PHP / JS / Python codebases usually use more CPU (and sometimes RAM but that’s less deterministic) resources than a 99% equivalent Elixir codebase so I started demonstrating it instead and except for one time (a custom-compiled Node.JS heavily tuned for I/O throughput) it was always a success that led to smaller hosting costs several months down the line. I know there are still many around here that would contest such a claim until the end of time but I know what I saw in various DataDog / NewRelic / other telemetry performance dashboard solutions.

  • LONG-TERM: Code maintainabiliy. This is not a given and depends on the team, a lot. One advice here is: ignore common wisdom and find your own best patterns. I know at least 4 different ways how people structure their project: none of them are “the best”! You should invest in quick code discoverability from the get go. Don’t get fixated on artificial constructs like models, controllers, views, templates, domain / business logic and many others – to clarify, they are a very good start but often get problematic once the project starts growing. Focus on this: “if I take a 3 months break, where I would first look for X if I have a task Y?” This question should be asked to each team member and the code should be organized according to a democratized average/mean value of the answers.

6 Likes

FWIW, Adopting Elixir has some fairly in-depth discussions / interviews about this exact topic.

7 Likes

I’m in the early stages of migrating a production app from rails to Phoenix using Terraform and it’s gone as smooth as i could have hoped, aside from an nginx gotcha I should probably post about in a separate thread.

It really felt miraculous how easy it was to extract the scattered, hidden bits of devise, jwt, and authorization infrastructure and drop it all into a few modules and plugs without having to spend much time at all on painful code organization questions.

I’ve been needing to work on bugfixes for the legacy code and adding new features at the same time so I feel like I’ve been rushing the migration and yet I think the result is still better than legacy code i put what I thought was a lot more time into.

As others have said the first step was getting the phoenix app running with Terraform and sending all API requests through it. Once I was confident that worked I added the multi tenancy logic (goodbye apartment :sweat_smile:) authentication and authorization, and then I’ve been moving endpoints one by one. I still handle all database stuff through the rails app, but it’s been pretty easy to get tests working and start creating schemas. I do think a temptation it has been important to resist is to try to bring more over for a given feature than is strictly necessary, because that leads pretty quickly to bikeshedding about the ‘right’ way to translate rails domain models into elixir whereas if i focus on just what is necessary to get things working phoenix provides plenty of guard rails to keep things sane and direct.

6 Likes

Thank you so much for the contributions in this thread so far. The fact that each comment mentions Terraform which I hadn’t heard of before - that’s worth the price of admission for this thread for me!

I’ll circle back with questions after I absorb, but what I’ve seen so far looks like excellent insight.

Hey @John-Goff - could you explain more about your docker setup? We currently use docker for our Node backend. Do you develop the Phoenix+Terraform setup alongside the Rails API with docker-compose and deploy it the same way?

Sure, so we are currently developing by running the servers locally, I have a bash script which just starts tmux and then runs all the commands needed. Then to deploy we build a new image for rails, Phoenix, and nginx with the frontend assets. Images are pushed to Amazon’s container registry and then pulled down on the target servers, where they are run using docker-compose. I do have it set up so you can run the docker-compose stack locally to debug issues with the production setup, but I found the overhead of docker, both mentally and computationally, to not be worth it in develop. At the moment I also have postgres running as a docker container alongside the other containers in compose, but I am planning on moving to a hosted postgres instance soon.

1 Like

Thanks John. We have a similar setup (GH actions → Azure container registry → Azure app services) but without docker-compose, which may be an unrealized potential. I like your idea of a bash script to just start everything up… maybe I should look into Tmux over iTerm panes for that reason.

I spoke to my engineering manager today and he’s supportive of getting Elixir in production, but really unsure of the overall sell toward an Elixir monolith. But we had previously spoken about moving one part of our Node monolith into an Elixir service (messaging :smiley: a classic use-case ). I expressed my prediction that we would like it so much that we might end up going all in, and the messaging “service” grows more. So maybe this will be my technical (and social) foot in the door.

I’m very interested in a script that starts and uses tmux to start several tasks in several panes. Have you written somewhere about it in more details?

I honestly am super uneducated about the pros and cons of docker-compose, it was set up by a previous team member who’s no longer here and I haven’t had any reason to switch what’s already working. That’s the only endorsement I can give though. Though I do wholly support using tmux over iterm panes, reason being is I can write the script once and all my team members can use it regardless of their platform.

Yep I think the “we like it so it swallowed our product” is a fairly typical adoption story from what I’ve heard. Best of luck with your transition!

1 Like

I have not no, but that’s a decent idea so I might. In the meantime it’s really super barebones, just calling tmux new-session and then tmux split-window a few times. At the end make sure you call tmux attach-session to connect to the session you just created. Whole thing is under 50 lines with the majority being setting up env vars to pass to the various servers depending on some flags you can give. Really like 6 lines of code are directly dealing with tmux and I’ve covered them all. You can read the tmux man pages for more info I’m sure, that’s where I got most all of this.

2 Likes