Elixir at Ramp 🧪

May 24, 2021

Many fintech startups have found Elixir to be an excellent fit for their needs — Divvy, Brex, Solarisbank AG, Teller, with the tradition extending back to Klarna using Erlang.

Hi, I’m Pablo! 👋 I’ve been an unapologetic fan of Erlang for the better part of the last decade, and one of Ramp’s first hires. When I was asked to design a few of our systems, I was delighted to choose Elixir as well. I’ll go over a few of the reasons it’s a great fit for a few of the systems we’ve rolled out.

Pablo at Elixir NYC

Me at ElixirNYC; one of the last things I did before lockdown.

The magic of BEAM and OTP…

Most “Why Elixir” posts go on to describe the features of BEAM and the OTP (Erlang’s virtual machine and a library of abstractions for reliable, concurrent systems, respectively). And for good reason! Whether you’re in Erlang or Elixir, the built-in resources for complex, distributed, high-reliability systems are unique compared to most programming environments and extremely valuable. I spoke at length about them at Deconstruct 2018. I’ll also bullet out some points here, with links to deeper dives if you’re curious:

  • Fantastic support for concurrency and parallelism that’s designed to avoid system failure. See The soul of Erlang and Elixir by Saša Jurić. Ask yourself as you watch “How would I implement a system with these properties in Ruby, Node, Python, or Java?”
  • Correctness guarantees for partial failures and transient errors (see It’s About Guarantees by Fred Hebert). Transient errors are extremely common and most environments don’t have a great mechanism for handling them.
  • Efficient string construction, especially useful in template rendering (Elixir RAM and the Template of Doom by Evan Miller).

Succinctly, BEAM is a battle-tested VM for resilient, concurrent applications, and OTP builds abstractions over it that are hard to find in other environments. These have done more to change how I think about programming systems than anything else I’ve encountered since graduating, and I miss these features in every other environment I’ve worked on since.

A necromancer labeled "Erlang," standing over an army of dead, saying "Destroy
one of my processes and I will only grow stronger"

(from this amazing comic)

…with a great, modern developer experience

So move it all to Erlang? Not quite. Even with the magic of OTP/BEAM, Erlang can be challenging to adopt. Amazing work has been done to improve this, from software like rebar3 to documentation like Adopting Erlang. While I highly recommend everyone give Erlang a try, many of the conveniences of Elixir made it easier to choose for a high-growth company setting.

First, the syntax is more familiar for programmers from other environments (here’s an example of Erlang compared to Elixir). Syntax isn’t personally interesting for me, and I happen to find Erlang beautiful, but I’d be in denial if I pretended I didn’t observe a meaningful negative reaction from others when I tried introducing them to Erlang.

Second, tools follow a stricter set of conventions, often with very human-friendly ergonomics. Some examples:

Finally (and this deserves a post of its own), consider the ergonomics afforded by having tools like a macro system and sigils. Macros are highly romanticized, and there’s much justified danger of using them too freely when a function will do; with that said, the flexibility can be used to great effect: much of Pheonix, Ecto, and ExUnit’s more declarative constructs come from the use of these.

A sweet, Sailor Moon-ey cute anime girl magician saying "I draw Eldritch Power
from all my process-chans!"

(source same as above. Also note that all the points 👆 aren't just beneficial in contrast to Erlang, but stand strong next to any other programming ecosystem)

Dynamic types, while avoiding the pitfalls

I’ll make a special mention for two specific features I miss most in environments like Python, Node, or Ruby: immutability and module structure with aliased calls. My hot take about most dynamic languages is that they are a poor fit for startups who have intentions of being long-term businesses: you’ve created an environment that’s optimized for your founding engineers to build something quickly in the first 7 months, but the price is a set of recurring obstacles which your engineers will pay down over the next 7 years. That microframework where all 4 of your engineers kept the first 10k LOC in their heads starts to be much harder to work with when you hit with 40 engineers and 400k LOC (and beyond! oh, and you have customers now, too).

Obviously companies have gone on to great success on all kinds of technologies, and getting to product-market fit matters a lot more than optimizing for life after it; but I don’t think it should be controversial to say when trying to understand or modify large systems, it helps to have invariants, and most dynamic languages don’t give you many. Elixir’s immutability shines here. Consider the following lines of Elixir:

defmodule MyApp.Widgets do

  alias MyApp.WidgetConnection

  def augment_widget(conn, w = %Widget{}) do
    abbreviation = WidgetConnection.find_abbreviation(conn, w)
    
    formats =
      abbreviation.signals
      |> Enum.map(&dependencies_for/1)
      |> Enum.filter(& applicable_to(&1, w))
  
      %{w | formats: formats}
  end

  # defp dependencies_for(signal) do
  # defp applicable_to(signal_deps, widget) do
end

compared to something like this in Python:

from myapp.widgetconnection import find_abbreviation

def augment_widget(conn: Connection, w: Widget) -> None:
  abbreviation = find_abbreviation(conn, w)
  mapped = [_dependencies_for(s) for s in abbreviation.signals]
  formats = [s for s in mapped if _applicable_to(s, w)]
  w.formats = formats

# def _dependencies_for(signal):
# def _applicable_to(signal_deps, widet):

Pay attention to what you know isn’t happening in the Elixir example:

  • You know conn and w will not change in the call to find_abbreviation. In most environments, you don’t know if your structures will mutate outside of where you can see them, so you’re not sure to what degree you can count on them further in the execution. In an immutable language, you can trust the data is the same for the whole function body while reading it.
  • We didn’t import WidgetConnection, we alias-ed it, meaning we merely added some syntactic sugar to a fully qualified function call. This, combined with the restriction that Elixir modules can’t contain code that executes at the toplevel, means we don’t have to worry about circular imports: In Elixir, both modules can alias each other, but in Python, you can’t have both import each other’s definitions.

So as the Elixir app grows and grows, an entire class of uncertainty over “what is this code in front of me doing” goes away, because there’s a ton of dangerous behavior you know it’s not doing. You’re not home free! Function calls to other modules could still call the network, or write to the DB, or delete files. But you’re certain it’s not changing the variables you are looking at, and you don’t need to keep a universe of state changes in your head to understand what the code is doing.

Additionally, “make that module’s code visible to this code” has a rash of problems we avoid as well. This can get complicated! Khan Academy once spent 2 months moving files around, and Instagram Engineering explicitly talks about wanting some of these contraints in Python’s module system. Here’s Dropbox Engineering describing “the tangle,” on how Python’s module loading complicated their type checker, since module dependencies can become impossible to tease out at scale. Using alias means not having to worry about a dependency tree in the definitions stage.

This is not to pick on Python in particular; just again to highlight a strength in a core design choice of Erlang and Elixir that made it appealing to me as a developer who’s been burnt on large codebases in other languages.

reverentgeek's graphic of Joe Armstrong, with a quote of his: "Make it work, then make it beautiful, then if you really, really have to, make it fast. 90% of the time, if you make it beautiful, it will already be fast. So just make it beautiful."

Joe Armstrong, drawn after his passing. (source). Also, one thing the other languages do better than Erlang/Elixir? Bolted-on type checkers. mypy or TypeScript or Sorbet have a lot of great qualities missing in the humble Dialyzer.

Augmenting, but choice of tech is only part of it

Ramp uses Elixir for a few specific, critical services: our real-time Authorization engine (which requires extremely high uptime) and our Risk analysis platform (requiring strong concurrency and data pipeline ingestion). That said, it isn’t what powers most of our business: our largest service is in Python, and our frontend is in TypeScript with React. When looking at how one can best work towards engineering success, rather than specific tech, I think it’s more important to ask:

Clearly, I love what Elixir allows me to do. But I’ll gleefully invest money and time with a company honoring the above properties over any technical choice they make. When I joined Ramp, there was a clear preference for Python: the founders previously built and scaled Paribus on it, and brought a lot of expertise with them. And not for nothing: Python can be an absolute joy to use ❤️ So our largest backend codebase is written in Python, and it’s where most of our developers spend their time.

But in service of the points above: our CTO asked me to design our real-time, high-uptime Authorization service, he created an environment where I felt safe enough to propose a different tech stack, and gave me the authority and ownership to move forward on it. It’s been exceptionally stable, flexible enough to allow for rapid feature development, and it’s some of the code I’m most proud of in my life. Good tech doesn’t merely coexist with good team culture and management, it depends on it, so as much fun as I have fun with tech talk like this post, it definitely has a ceiling on its utility when building a business.

I hope I’ve piqued your interest in trying Elixir if you weren’t considering it before 😄 Also (and you knew it was coming), please reach out if you’re looking for your next gig! The magic of the last two years with this company has been its people, and we’re always looking for the next ones to keep building with ❤️

And introduce us whoever manages spend at your company! Our product rules: we’ve helped thousands of customers like Ro, Better.com and ClickUp identify over $10M in wasteful spend, eliminate manual + broken expense reports, and close their books 86% faster. Users love the impact our software drives - we’re the #1 rated spend management vendor on G2. Put us in touch with your finance and accounting teams so that we can help your company spend more intelligently!

Smart is the new platinum.
© 2021 Ramp Business Corporation. “Ramp,” "Ramp Financial" and the Ramp logo are trademarks of the company.

The Ramp Visa® Commercial Card is issued by Sutton Bank, Member FDIC. Terms and conditions apply.

Please visit our Terms of Service for more details.