Elixir Cheatsheet
Processes and OTP
Use this Elixir reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Processes
parent = self() pid = spawn(fn -> send(parent, {:done, 42}) end) receive do {:done, value} -> value after 1000 -> :timeout end Process.alive?(pid) Process.sleep(100)
BEAM processes are lightweight (thousands are normal), isolated, and communicate only through message passing. receive pattern matches against the process mailbox.
Links vs Monitors
spawn_link(fn -> raise "boom" end) # link: crash propagates to the caller # (unless it traps exits) {:ok, pid} = start_worker() ref = Process.monitor(pid) # monitor: one-way, get a message instead receive do {:DOWN, ^ref, :process, ^pid, reason} -> {:worker_died, reason} end Process.flag(:trap_exit, true) # convert exit signals to {:EXIT, pid, reason} messages
Rule: link when the processes should live and die together (supervision trees are built on links); monitor when you only want to be notified.
Tasks
task = Task.async(fn -> expensive_work() end) other_work() Task.await(task, 5_000) # raises on timeout/crash # run a collection concurrently with bounded concurrency urls |> Task.async_stream(&fetch/1, max_concurrency: 8, timeout: 10_000) |> Enum.map(fn {:ok, result} -> result end) # fire-and-forget under a supervisor (survives caller, restarts nothing) Task.Supervisor.start_child(MyApp.TaskSup, fn -> send_email(user) end) Task.Supervisor.async_nolink(MyApp.TaskSup, fn -> risky() end)
Task.async/1 links to the caller — a crash takes the caller down. Use Task.Supervisor (started in your supervision tree as {Task.Supervisor, name: MyApp.TaskSup}) for fire-and-forget or non-linked async work, and Task.async_stream/3 for parallel map over a collection.
Agent
{:ok, pid} = Agent.start_link(fn -> 0 end, name: MyCounter)
Agent.update(MyCounter, &(&1 + 1))
Agent.get(MyCounter, & &1)
Agent.get_and_update(MyCounter, fn n -> {n, n + 1} end)
Agent.stop(MyCounter)An Agent is a process that owns a single state value — fine for simple shared state; use a GenServer once you need a real protocol.
GenServer
defmodule Counter do use GenServer # Client API def start_link(opts) do initial = Keyword.get(opts, :initial, 0) GenServer.start_link(__MODULE__, initial, name: __MODULE__) end def inc, do: GenServer.cast(__MODULE__, :inc) # async, no reply def value, do: GenServer.call(__MODULE__, :value) # sync, waits for reply # Server callbacks @impl true def init(count) do :timer.send_interval(60_000, :tick) {:ok, count} end @impl true def handle_call(:value, _from, count), do: {:reply, count, count} @impl true def handle_cast(:inc, count), do: {:noreply, count + 1} @impl true def handle_info(:tick, count) do # plain messages (timers, monitors) IO.puts("count is #{count}") {:noreply, count} end end
Public functions are the client API; @impl true callbacks run inside the server process. call for queries and anything needing backpressure, cast for fire-and-forget, handle_info for raw send/timer/:DOWN messages.
Name Registration
GenServer.start_link(__MODULE__, arg, name: __MODULE__) # one global instance GenServer.start_link(__MODULE__, arg, name: {:via, Registry, {MyApp.Registry, "room:#{id}"}}) # one per key # in the supervision tree: {Registry, keys: :unique, name: MyApp.Registry} # lookup [{pid, _}] = Registry.lookup(MyApp.Registry, "room:42")
name: __MODULE__ suits singletons. Registry gives you dynamic, per-key names (rooms, sessions, device IDs) without atom leaks.
Supervision Trees
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ {Registry, keys: :unique, name: MyApp.Registry}, {Task.Supervisor, name: MyApp.TaskSup}, {Counter, initial: 0}, {DynamicSupervisor, name: MyApp.RoomSup, strategy: :one_for_one} ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end end
Strategies: :one_for_one (restart only the crashed child), :one_for_all (restart all), :rest_for_one (restart it and those started after it). Child restart types: restart: :permanent (default) | :transient (restart only on abnormal exit) | :temporary.
DynamicSupervisor
DynamicSupervisor.start_child(MyApp.RoomSup, {RoomServer, room_id}) DynamicSupervisor.terminate_child(MyApp.RoomSup, pid) DynamicSupervisor.count_children(MyApp.RoomSup)
Use a DynamicSupervisor when children are started on demand at runtime (one process per game room, connection, or user session), usually paired with Registry via-names.
OTP Rule of Thumb
Use plain functions for pure transformations, Task for one-off async work, Agent for trivially simple owned state, GenServer when state has commands/queries/timers, Registry + DynamicSupervisor for many runtime-spawned servers — and always start long-lived processes under a supervisor, never bare spawn.