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.

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.