Day 2 of 5
⏱ ~60 minutes
Elixir in 5 Days — Day 2

Processes & Concurrency

Elixir's killer feature is concurrency. BEAM processes are lighter than OS threads — you can spawn a million of them. Today you learn to spawn processes, send messages, and build supervised process trees.

Spawning Processes

spawn/1 creates a new process running a function. Each process has a unique PID. Processes communicate only by sending messages — no shared memory. send(pid, message) sends; receive do...end receives. self() returns the current PID. Process.alive?(pid) checks if a process is running. spawn_link/1 creates a bidirectional link — if one crashes, the other crashes too.

GenServer: The Building Block

GenServer (Generic Server) is a behaviour (interface) that implements a client-server process pattern. You define handle_call/3 (synchronous), handle_cast/2 (async), init/1, and handle_info/2. GenServer.start_link/2 starts it; GenServer.call/2 sends synchronous messages; GenServer.cast/2 sends async. All state lives in the GenServer process — no shared state across processes, no locks needed.

OTP Supervisors

A Supervisor monitors child processes and restarts them on crash. Restart strategies: one_for_one (restart only the crashed child), one_for_all (restart all if one crashes), rest_for_one (restart crashed + those started after it). Supervisors form trees — the top-level supervisor rarely crashes, and lower-level process crashes are contained and auto-recovered. This is the OTP fault-tolerance model.

elixir
defmodule Counter do
  use GenServer

  # Client API
  def start_link(initial \\ 0) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment, do: GenServer.cast(__MODULE__, :increment)
  def decrement, do: GenServer.cast(__MODULE__, :decrement)
  def value,     do: GenServer.call(__MODULE__, :value)

  # Server callbacks
  @impl true
  def init(count), do: {:ok, count}

  @impl true
  def handle_call(:value, _from, count) do
    {:reply, count, count}
  end

  @impl true
  def handle_cast(:increment, count), do: {:noreply, count + 1}
  def handle_cast(:decrement, count), do: {:noreply, count - 1}
end

# Usage
{:ok, _pid} = Counter.start_link(0)
Counter.increment()
Counter.increment()
Counter.increment()
IO.puts Counter.value()  # 3
💡
Use GenServer.call/2 for operations where you need a response. Use GenServer.cast/2 for fire-and-forget operations. Never do slow work inside handle_call — it blocks all callers during that time.
📝 Day 2 Exercise
Build a Concurrent Cache
  1. Create a new Mix project: mix new cache
  2. Implement a Cache GenServer that stores a map of key-value pairs
  3. Add get/1, put/2, and delete/1 client functions
  4. Add a TTL (time-to-live): entries expire after 60 seconds using Process.send_after
  5. Test it: put 5 keys, get them back, wait for expiry, verify they are gone

Day 2 Summary

  • Elixir processes are isolated — a crash cannot corrupt another process's state
  • Processes communicate via message passing only — no shared memory
  • GenServer provides a structured client-server process pattern
  • Supervisors automatically restart crashed child processes
  • OTP fault-tolerance: let it crash, then recover automatically
Challenge

Build a rate limiter GenServer that allows at most N requests per minute per client ID. If the limit is exceeded, return {:error, :rate_limited}. Test with 5 concurrent processes each making 20 requests.

Finished this lesson?