๐Ÿ’ง

A language tour

The Concurrency of Elixir

Millions of lightweight processes. Fault-tolerant by design. The Erlang VM wearing a modern face.

scroll

01 โ€” Processes

Millions of actors, no shared state

Elixir processes are not OS threads โ€” they're lightweight actors managed by the BEAM virtual machine. You can spawn millions of them. They share no memory. They communicate by sending messages. Concurrency becomes simple when state can't be shared.

"Elixir is the language I wish I had when I was building Ruby on Rails. It's pragmatic, productive, and brings the power of the Erlang ecosystem to a modern, friendly syntax."

โ€” Josรฉ Valim, creator of Elixir
processes.ex
# Spawn a lightweight process
pid = spawn(fn ->
  receive do
    {:greet, name} -> IO.puts("Hello, #{name}!")
  end
end)

# Send a message โ€” async, non-blocking
send(pid, {:greet, "Elixir"})
# "Hello, Elixir!"

# Spawn 100,000 processes โ€” this is fast and cheap
pids = for i <- 1..100_000 do
  spawn(fn -> Process.sleep(1000); i * i end)
end

Each Elixir process has its own garbage-collected heap. The BEAM scheduler runs them cooperatively across all CPU cores. 100,000 processes is not unusual โ€” it's the model.


02 โ€” The Pipe Operator

Data flowing through a pipeline

The pipe operator |> passes the result of one expression as the first argument to the next function. It transforms nested, inside-out code into a linear, readable pipeline โ€” left to right, top to bottom.

pipe.ex
# Without pipe โ€” nested, inside-out
String.upcase(String.trim(String.reverse("  hello  ")))

# With pipe โ€” reads like a sentence
"  hello  "
|> String.reverse
|> String.trim
|> String.upcase
# "OLLEH"

# Data processing pipeline
[1, 2, 3, 4, 5, 6]
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.map(fn x -> x * x end)
|> Enum.sum
# 56

The pipe operator transforms the mental model from "call functions on data" to "data flowing through transformations." It's the same computation โ€” the readability difference is significant.


03 โ€” Pattern Matching

Destructure anything, everywhere

Pattern matching in Elixir isn't just a switch statement โ€” the = sign itself is a match operator. It binds variables and asserts structure simultaneously. It's how you unpack function arguments, handle errors, and destructure data.

pattern_matching.ex
# = is a match operator, not assignment
{a, b, c} = {1, 2, 3}   # a=1, b=2, c=3
[head | tail] = [1,2,3]  # head=1, tail=[2,3]

# Function clauses match on arguments
defmodule Greeter do
  def greet({:ok, name}),    do: "Hello, #{name}!"
  def greet({:error, reason}), do: "Error: #{reason}"
  def greet(:anonymous),      do: "Hello, stranger!"
end

Greeter.greet({:ok, "Josรฉ"})  # "Hello, Josรฉ!"
Greeter.greet(:anonymous)     # "Hello, stranger!"

Multiple function clauses with different patterns is Elixir's idiomatic way of handling cases. The runtime dispatches to the first clause whose pattern matches โ€” a cleaner alternative to if/else chains.


04 โ€” OTP & Supervisors

Fault tolerance built in

OTP is a set of libraries and design patterns for building fault-tolerant systems. Supervisors watch processes and restart them when they crash. You don't write defensive code โ€” you let it crash and rely on the supervision tree to recover.

supervisor.ex
defmodule Cache do
  use GenServer  # Generic Server behaviour

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  def init(state), do: {:ok, state}

  def put(key, val), do: GenServer.cast(__MODULE__, {:put, key, val})
  def get(key),      do: GenServer.call(__MODULE__, {:get, key})

  def handle_cast({:put, k, v}, state), do: {:noreply, Map.put(state, k, v)}
  def handle_call({:get, k}, _from, state), do: {:reply, state[k], state}
end

# Supervisor restarts Cache if it crashes โ€” automatically

A GenServer is an ordinary Elixir process with a standard message-handling protocol. The OTP framework handles the boilerplate โ€” timeouts, calls vs. casts, initialisation โ€” leaving you to describe the state transitions.


05 โ€” Immutability

Data never changes

All data in Elixir is immutable. You don't modify a map โ€” you create a new one with the change applied. This makes reasoning about concurrent code trivial: if data can't change, it can't be corrupted by a concurrent process.

immutability.ex
# Maps are immutable โ€” updates return new maps
user = %{name: "Alice", age: 30, role: :admin}

# Create a new map with updated age
older_user = %{user | age: 31}

# user is unchanged
IO.inspect(user.age)       # 30
IO.inspect(older_user.age)  # 31

# Rebinding a variable doesn't mutate โ€” it creates a new binding
x = 1
x = 2   # x now points to a new value; the old 1 is unchanged

# Deep update with put_in
settings = %{db: %{host: "localhost", port: 5432}}
put_in(settings, [:db, :port], 5433)

%{user | age: 31} is a structural update โ€” the BEAM shares unchanged parts of the old map with the new one. Immutability doesn't mean copying everything; it means never mutating shared memory.


06 โ€” The Whole Picture

Why Elixir is compelling

๐ŸŒ

Phoenix

Phoenix LiveView builds real-time interactive web apps without JavaScript, using server-side rendering over WebSockets.

๐Ÿ“ก

WhatsApp Scale

Erlang/OTP powered WhatsApp to 2 billion users. Elixir runs on the same VM โ€” those guarantees transfer.

๐Ÿ”„

Hot Code Upgrades

Deploy new code to a running system without stopping it. A feature from telecom systems, now for web services.

๐Ÿงช

ExUnit

Built-in testing framework with excellent assertion messages. Testing is first-class in the Elixir ecosystem.

๐Ÿ“ฆ

Mix & Hex

Mix is the build tool. Hex is the package manager. Both are excellent and built into the core toolchain.

๐Ÿญ

Nerves

Run Elixir on embedded hardware. Fault-tolerant IoT firmware with the full OTP supervision model.