# GenServer Patterns — From the Elixir Source Analysis of `lib/elixir/lib/gen_server.ex`, `lib/elixir/lib/agent.ex`, and related modules. --- ## Pattern 1: Client/Server API Separation **Source:** `lib/elixir/lib/gen_server.ex:101-149` (documentation example) **What it does:** Every GenServer module defines two distinct API layers — a **client API** (thin public functions that wrap `GenServer.call/cast`) and a **server API** (callback implementations). The client functions live in the same module but are clearly separated with comments. **Why:** Encapsulation. Callers don't need to know the message protocol. The client API provides a typed, documented interface while the server callbacks handle the actual logic. This allows changing the internal message format without breaking callers. **Anti-pattern:** Calling `GenServer.call(MyServer, :some_msg)` directly from other modules. This leaks the message protocol and couples callers to implementation details. **Code example from source:** ```elixir defmodule Stack do use GenServer # Client def start_link(default) when is_binary(default) do GenServer.start_link(__MODULE__, default) end def push(pid, element) do GenServer.cast(pid, {:push, element}) end def pop(pid) do GenServer.call(pid, :pop) end # Server (callbacks) @impl true def init(elements) do initial_state = String.split(elements, ",", trim: true) {:ok, initial_state} end @impl true def handle_call(:pop, _from, state) do [to_caller | new_state] = state {:reply, to_caller, new_state} end @impl true def handle_cast({:push, element}, state) do new_state = [element | state] {:noreply, new_state} end end ``` --- ## Pattern 2: `@impl true` Annotations on All Callbacks **Source:** `lib/elixir/lib/gen_server.ex:41-60` (Stack example) **What it does:** Every callback function is annotated with `@impl true`. This tells the compiler to verify that the function is a valid callback for the declared behaviour. **Why:** Catches typos and mismatches at compile time. If you accidentally name a callback `handle_calls` instead of `handle_call`, the compiler will warn you. It also serves as documentation for readers — you immediately know which functions are callbacks vs. helper functions. **Anti-pattern:** Omitting `@impl true` on callbacks, especially when mixing callbacks with private helpers in the same module. **Code example from source:** ```elixir @impl true def init(counter) do {:ok, counter} end @impl true def handle_call(:get, _from, counter) do {:reply, counter, counter} end ``` --- ## Pattern 3: Guard-Protected `start_link` **Source:** `lib/elixir/lib/gen_server.ex:101` and `lib/elixir/lib/agent.ex:28` **What it does:** The `start_link` function uses guards to validate its arguments at the API boundary, failing fast with a clear error before any process spawning occurs. **Why:** Catches invalid arguments immediately at the caller site, not deep inside `init/1` where the error would be harder to trace. This is the fail-fast principle applied at process boundaries. **Anti-pattern:** Accepting any term in `start_link` and then crashing inside `init/1` with a confusing `FunctionClauseError` or `MatchError`. **Code example from source:** ```elixir # From gen_server.ex documentation def start_link(default) when is_binary(default) do GenServer.start_link(__MODULE__, default) end # From agent.ex:246 @spec start_link((-> term), GenServer.options()) :: on_start def start_link(fun, options \\ []) when is_function(fun, 0) do GenServer.start_link(Agent.Server, fun, options) end ``` --- ## Pattern 4: `handle_continue` for Post-Init Work **Source:** `lib/elixir/lib/gen_server.ex:520-528` (callback spec), `lib/elixir/lib/gen_server.ex:714-720` (handle_continue callback definition) **What it does:** `init/1` can return `{:ok, state, {:continue, continue_arg}}`, which causes `handle_continue/2` to be invoked immediately after init completes. This allows splitting initialization into a synchronous part (that unblocks the supervisor) and an asynchronous continuation. **Why:** `init/1` blocks the supervisor — if it does expensive work (DB connections, HTTP calls, cache warming), it delays the entire supervision tree startup. `handle_continue` lets you return quickly from `init/1` while still performing setup before handling any client messages. Unlike `send(self(), :continue)`, it's guaranteed to execute before any other messages in the mailbox. **Anti-pattern:** Doing slow initialization directly in `init/1`, blocking the supervisor. Or using `send(self(), :init_continue)` which doesn't guarantee ordering — a client message could arrive first. **Code example from source (callback spec):** ```elixir @callback handle_continue(continue_arg, state :: term) :: {:noreply, new_state} | {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg}} | {:stop, reason :: term, new_state} when new_state: term, continue_arg: term ``` **Usage pattern:** ```elixir def init(args) do # Quick setup only — return immediately to unblock supervisor {:ok, %{config: args, data: nil}, {:continue, :load_data}} end @impl true def handle_continue(:load_data, state) do # Expensive work happens here, guaranteed before any call/cast data = expensive_database_query() {:noreply, %{state | data: data}} end ``` --- ## Pattern 5: Timeout-Based Idle Shutdown **Source:** `lib/elixir/lib/gen_server.ex:335-380` **What it does:** Callbacks can return a timeout value (milliseconds) as the last element of the return tuple. If no message arrives within that time, `handle_info(:timeout, state)` is called. This enables idle process cleanup. **Why:** Prevents resource leaks from idle processes. A process that's not being used can shut itself down gracefully, freeing memory and reducing the supervision tree. Useful for connection pools, caches, and session processes. **Anti-pattern:** Using `Process.send_after` for idle detection — it doesn't reset on activity. The built-in timeout resets every time a message arrives, which is the correct semantic for "idle detection." **Code example from source:** ```elixir defmodule Counter do use GenServer @timeout to_timeout(second: 5) @impl true def init(count) do {:ok, count, @timeout} end @impl true def handle_call(:increment, _from, count) do new_count = count + 1 {:reply, new_count, new_count, @timeout} end @impl true def handle_info(:timeout, count) do {:stop, :normal, count} end end ``` --- ## Pattern 6: Periodic Work via `Process.send_after` **Source:** `lib/elixir/lib/gen_server.ex:298-332` (Periodically example in docs) **What it does:** A GenServer schedules periodic work by sending itself a message via `Process.send_after` in `init/1`, then rescheduling in `handle_info`. This creates a self-sustaining periodic loop. **Why:** Unlike timeouts (which reset on any message), `send_after` gives precise control over scheduling intervals. The pattern is self-healing — if the process crashes and restarts, `init` reschedules automatically. It's simpler than external timer libraries. **Anti-pattern:** Using `:timer.send_interval` — if the GenServer is restarted by its supervisor, the timer from the old process keeps firing into the void (the timer is tied to the process that created it, but `send_interval` isn't linked to the destination). `send_after` in `handle_info` naturally dies with the process. **Code example from source:** ```elixir defmodule MyApp.Periodically do use GenServer def start_link(_) do GenServer.start_link(__MODULE__, %{}) end @impl true def init(state) do schedule_work() {:ok, state} end @impl true def handle_info(:work, state) do # Do the desired work here schedule_work() {:noreply, state} end defp schedule_work do Process.send_after(self(), :work, 2 * 60 * 60 * 1000) end end ``` --- ## Pattern 7: Call vs Cast Decision (Synchronous vs Asynchronous) **Source:** `lib/elixir/lib/gen_server.ex:83-90` (docs), `lib/elixir/lib/agent.ex:368-378` (Agent.update uses call, Agent.cast uses cast) **What it does:** The Elixir team uses `call` (synchronous) for operations where the client needs confirmation or a return value, and `cast` (fire-and-forget) only when the client genuinely doesn't care about the outcome. **Why:** `cast` provides no backpressure — if a producer sends casts faster than the GenServer can process them, the mailbox grows unbounded until OOM. `call` naturally provides backpressure because the caller blocks. The Agent module makes this explicit: `Agent.update/3` uses `GenServer.call` (not cast!) because updates need confirmation of ordering. **Anti-pattern:** Using `cast` for operations that should have backpressure, or using `cast` when you actually need to know if the operation succeeded. The Agent source proves this — even "fire and forget" `Agent.update` is a `call`: **Code example from source (agent.ex):** ```elixir # Agent.update uses call — NOT cast — for backpressure @spec update(agent, (state -> state), timeout) :: :ok def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do GenServer.call(agent, {:update, fun}, timeout) end # Agent.cast is the explicit fire-and-forget variant @spec cast(agent, (state -> state)) :: :ok def cast(agent, fun) when is_function(fun, 1) do GenServer.cast(agent, {:cast, fun}) end ``` --- ## Pattern 8: Default Callback Implementations with Clear Error Messages **Source:** `lib/elixir/lib/gen_server.ex:902-993` (`__using__` macro) **What it does:** `use GenServer` injects default implementations of `handle_call`, `handle_cast`, `handle_info`, `terminate`, and `code_change`. The defaults for `handle_call` and `handle_cast` raise with a descriptive error message including the process name. `handle_info` logs an error. All are `defoverridable`. **Why:** This is defensive programming at the framework level. Instead of getting a cryptic `:function_clause` error from the Erlang runtime, developers get "attempted to call GenServer #{inspect(proc)} but no handle_call/3 clause was provided". The defaults also mean you only implement the callbacks you actually need. **Anti-pattern:** Not implementing `handle_info` when your process might receive unexpected messages (monitors, nodedown, etc). The default logs a warning but doesn't crash — this is intentional. **Code example from source:** ```elixir @doc false def handle_call(msg, _from, state) do proc = case Process.info(self(), :registered_name) do {_, []} -> self() {_, name} -> name end case :erlang.phash2(1, 1) do 0 -> raise "attempted to call GenServer #{inspect(proc)} but no handle_call/3 clause was provided" 1 -> {:stop, {:bad_call, msg}, state} end end @doc false def handle_info(msg, state) do proc = case Process.info(self(), :registered_name) do {_, []} -> self() {_, name} -> name end :logger.error(...) {:noreply, state} end ``` --- ## Pattern 9: `child_spec/1` Generation and Customization via `use` Options **Source:** `lib/elixir/lib/gen_server.ex:900-921`, `lib/elixir/lib/agent.ex:206-218`, `lib/elixir/lib/task.ex:282-292` **What it does:** Each `use GenServer/Agent/Task` generates a `child_spec/1` function with sensible defaults that can be customized via options passed to `use`. The child spec is a map with `:id`, `:start`, `:restart`, `:shutdown`, and `:type`. **Why:** Encapsulates supervision configuration within the module itself. Supervisors just need `{MyModule, arg}` — they don't need to know restart strategies or shutdown timeouts. Different modules have different defaults: GenServer/Agent default to `:permanent` restart, Task defaults to `:temporary`. **Anti-pattern:** Defining child specs in the supervisor module rather than in the child module. This scatters configuration and makes modules non-portable between supervision trees. **Code example from source:** ```elixir # GenServer child_spec — restart: :permanent (default) def child_spec(init_arg) do default = %{ id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]} } Supervisor.child_spec(default, unquote(Macro.escape(opts))) end # Task child_spec — restart: :temporary (tasks don't restart) def child_spec(arg) do %{ id: Task, start: {Task, :start_link, [arg]}, restart: :temporary } end ``` --- ## Pattern 10: Agent as Minimal State Wrapper (GenServer Under the Hood) **Source:** `lib/elixir/lib/agent.ex:1-60` (module docs), `lib/elixir/lib/agent.ex:246-250` (implementation) **What it does:** Agent is implemented entirely in terms of `GenServer.start_link(Agent.Server, fun, options)`. It's a thin abstraction that provides `get/update/get_and_update/cast` over GenServer's `call/cast`. **Why:** When your process is purely about state (no complex message handling, no multi-step workflows), Agent removes boilerplate. It's an intentional design choice by the Elixir team: provide the simplest possible stateful process, built on the same foundation. **Anti-pattern:** Using Agent for anything that needs custom message handling, multi-step coordination, or where you'd benefit from `handle_info` (timers, monitors). If you need more than get/update, use GenServer directly. **Code example from source:** ```elixir # Agent is literally just GenServer with a purpose-built server module @spec start_link((-> term), GenServer.options()) :: on_start def start_link(fun, options \\ []) when is_function(fun, 0) do GenServer.start_link(Agent.Server, fun, options) end # Client/server boundary in Agent: expensive work placement matters # Compute in the agent/server (blocks the agent for all other callers): def get_something(agent) do Agent.get(agent, fn state -> do_something_expensive(state) end) end # Compute in the agent/client (copies state but doesn't block): def get_something(agent) do Agent.get(agent, & &1) |> do_something_expensive() end ``` --- ## Pattern 11: Name Registration via `:via` Tuple **Source:** `lib/elixir/lib/gen_server.ex:1087-1107` (do_start implementation), `lib/elixir/lib/gen_server.ex:230-250` (documentation) **What it does:** GenServer supports four naming schemes: `nil` (anonymous), atom (local), `{:global, term}` (cluster-wide), and `{:via, module, term}` (pluggable registry). The implementation delegates to `:gen.start` with the appropriate format. **Why:** The `:via` pattern enables dynamic process naming without atom leaks. Since atoms are never garbage-collected, dynamic names (like per-user or per-session processes) must use `{:via, Registry, {registry, key}}` to avoid exhausting the atom table. **Anti-pattern:** Using `String.to_atom("user_#{id}")` for dynamic process names. This creates atoms that are never GC'd and will eventually crash the VM. **Code example from source:** ```elixir # From gen_server.ex do_start/4 defp do_start(link, module, init_arg, options) do case Keyword.pop(options, :name) do {nil, opts} -> :gen.start(:gen_server, link, module, init_arg, opts) {atom, opts} when is_atom(atom) -> :gen.start(:gen_server, link, {:local, atom}, module, init_arg, opts) {{:global, _term} = tuple, opts} -> :gen.start(:gen_server, link, tuple, module, init_arg, opts) {{:via, via_module, _term} = tuple, opts} when is_atom(via_module) -> :gen.start(:gen_server, link, tuple, module, init_arg, opts) end end ``` --- ## Pattern 12: GenServer as Anti-Pattern — Don't Use Processes for Code Organization **Source:** `lib/elixir/lib/gen_server.ex:381-415` ("When (not) to use a GenServer" section) **What it does:** The Elixir team explicitly documents that GenServer should NOT be used for code organization. A process must model a runtime property: mutable state, concurrency boundary, or failure isolation. **Why:** A GenServer serializes all access through a single process. Using it for stateless computation (like a calculator) creates artificial bottlenecks. Processes have overhead (memory, scheduling, message copying). Functions are free. **Anti-pattern (from the source itself):** ```elixir # DON'T DO THIS — from gen_server.ex docs def add(a, b) do GenServer.call(__MODULE__, {:add, a, b}) end def handle_call({:add, a, b}, _from, state) do {:reply, a + b, state} end # DO THIS instead: def add(a, b) do a + b end ```