When I started building software professionally we defaulted to:
Everything being mutable
everything being nullable
everything being imperative
We ended up with trivial problems but complex software - more often than not.
Add in a bunch of people who can step on each other with ease, and you have a perfect recipe for disaster. We were drowning in regressions and incidents.
That made me question the status quo and look at alternatives. That took me into the world of FP
Although I never had a chance to work in an FP language, the things I learned from it haven't left me to this day and have made me a better programmer.
In this exploration, I found two languages that felt more accessible than others and had great web dev support (my day job) - Elixir and Clojure.
In this blog, I want to share my learnings about Elixir!
Accessible Syntax
The first thing that stood out for me in Elixir was the familiar syntax:
# everything inside module
defmodule Calcuator do
# do .. end , instead of {}
def sum(a, b) do
# implicit return
a + b
end
def multiply(a, b) do
a * b
end
end
This didn't feel as foreign to me as something like Haskell, Clojure, and OCaml did.
This feature is often overlooked. For experienced programmers like me, syntax isn’t a big deal. But for beginners, a familiar-looking language makes it much easier to get started.
Immutability and the pipe operator
Given it's a functional language all data structures are by default immutable.
This needs a different mindset: focus on transformation, not just changes.
This is often cumbersome to do. But the pipe operator makes it very obvious:
result =
2
|> Calcuator.sum(3) # calculator module from above
|> Calcuator.multiply(4)
Other languages use a lot of syntax shortcuts that hide how functional programming works without mutations.
Concurrency as first-class citizen
The software we built was slow.
There are several reasons for this, such as:
Legacy code to untangle
Performance not being a priority
Regressions and maintenance overhead
.. etc.
This is just the tip of the iceberg. The real challenge is that concurrency is hard.
Even with optimized platforms and languages, end users often don’t see the best performance. Here's the paradox:
Modern platforms are fast and cheap.
Initially, performance issues aren’t noticeable.
As we add more features, the app slowly becomes slower.
Once it reaches a threshold, fixing the slow parts becomes extremely difficult.
Better hardware actually makes it harder to build faster software.
On the other hand, there's the classic saying:
Premature optimization is the root of all evil.
This contradiction makes building fast software challenging.
Why Elixir makes this easier
In most languages, optimizing for concurrency requires major changes. For example, in JavaScript, adding async/await
means updating every part of the code using that resource—a huge refactor for complex projects.
Concurrent code also introduces challenges like observability, error handling, streaming, and maintaining correctness and availability. Synchronous code is simpler but has its limits.
Elixir handles concurrency more gracefully, making optimization easier—but not automatic. In Elixir land though the growth in complexity is graceful:
Pic worth a thousand words:
PS: I stole this from Effect
Elixir has a feature called processes—different from system processes or threads. They're extremely lightweight, start in microseconds, and use very little memory. Plus, they run on multiple cores!
This is a game-changer: processes are not just efficient but also a core part of the language.
Simple example:
pid = spawn(fn -> 1 + 2 end)
This will just spin up a separate process execute and die off.
We can spawn such processes up to a million in decent hardware. That is incredible.
OTP (Open Telecom Platform)
Earlier, I mentioned the challenges of working with concurrent code:
Observability
Reliability
Error handling
Scaling
Usually, we rely on external tools like Kubernetes, Grafana, or Kibana to address these issues.
In Elixir, the OTP libraries are built right into the language. With just a few lines of code, you can create powerful infrastructure-like setups directly in your app.
For example, Kubernetes has pods that restart automatically if they fail. In Elixir, you can achieve this with the Supervisor behavior. A supervisor monitors processes and restarts them if they go down—right within the language!
This doesn't replace external tools entirely, but Elixir gives you powerful built-in features before you need them.
BEAM (Bogdan's Erlang Abstract Machine)!
Finally, everything runs on a battle-tested VM (BEAM), which powers all this functionality.
The VM even works across networks, so concurrency can span multiple servers and cores—abstracted away from developers. You just spawn a process and get results; BEAM handles the rest (with some setup).
What sets Elixir apart is its tight integration between the language and tools. This unified platform lets you build advanced applications without needing complex infrastructure early on.
You can skip writing YAML files for a long time—maybe forever! And if you do need them, congratulations, your app is very successful!
Conclusion:
Well, I always found these languages underappreciated and I have a soft spot for the underdogs. I hope you all give this language a shot!