Elixir OTP Patterns
Master OTP (Open Telecom Platform) patterns to build concurrent, fault-tolerant Elixir applications. This skill covers GenServer, Supervisor, Agent, Task, and other OTP behaviors.
GenServer Basics defmodule Counter do use GenServer
# Client API
def start_link(initial_value \ 0) do GenServer.start_link(MODULE, initial_value, name: MODULE) end
def increment do GenServer.cast(MODULE, :increment) end
def get_value do GenServer.call(MODULE, :get_value) end
# Server Callbacks
@impl true def init(initial_value) do {:ok, initial_value} end
@impl true def handle_call(:get_value, _from, state) do {:reply, state, state} end
@impl true def handle_cast(:increment, state) do {:noreply, state + 1} end end
Usage
{:ok, _pid} = Counter.start_link(0) Counter.increment() Counter.get_value() # => 1
GenServer with State Management defmodule UserCache do use GenServer
# Client API
def start_link(_opts) do GenServer.start_link(MODULE, %{}, name: MODULE) end
def put(user_id, user_data) do GenServer.cast(MODULE, {:put, user_id, user_data}) end
def get(user_id) do GenServer.call(MODULE, {:get, user_id}) end
def delete(user_id) do GenServer.cast(MODULE, {:delete, user_id}) end
def all do GenServer.call(MODULE, :all) end
# Server Callbacks
@impl true def init(_opts) do {:ok, %{}} end
@impl true def handle_call({:get, user_id}, _from, state) do {:reply, Map.get(state, user_id), state} end
@impl true def handle_call(:all, _from, state) do {:reply, state, state} end
@impl true def handle_cast({:put, user_id, user_data}, state) do {:noreply, Map.put(state, user_id, user_data)} end
@impl true def handle_cast({:delete, user_id}, state) do {:noreply, Map.delete(state, user_id)} end end
Supervisor Strategies defmodule MyApp.Application do use Application
@impl true def start(_type, _args) do children = [ # One-for-one: restart only failed child {Counter, 0}, {UserCache, []},
# One-for-all supervisor
{Supervisor,
strategy: :one_for_all,
name: MyApp.CriticalSupervisor,
children: [
{Database, []},
{Cache, []}
]},
# Rest-for-one supervisor
{Supervisor,
strategy: :rest_for_one,
name: MyApp.OrderedSupervisor,
children: [
{ConfigLoader, []},
{DatabasePool, []},
{WebServer, []}
]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end end
Dynamic Supervisor defmodule TaskRunner do use GenServer
def start_link(task_id) do GenServer.start_link(MODULE, task_id) end
@impl true def init(task_id) do Process.send_after(self(), :run_task, 0) {:ok, task_id} end
@impl true def handle_info(:run_task, task_id) do # Perform task work IO.puts("Running task #{task_id}") {:noreply, task_id} end end
defmodule TaskSupervisor do use DynamicSupervisor
def start_link(_opts) do DynamicSupervisor.start_link(MODULE, :ok, name: MODULE) end
def start_task(task_id) do spec = {TaskRunner, task_id} DynamicSupervisor.start_child(MODULE, spec) end
def stop_task(pid) do DynamicSupervisor.terminate_child(MODULE, pid) end
@impl true def init(:ok) do DynamicSupervisor.init(strategy: :one_for_one) end end
Usage
TaskSupervisor.start_link([]) {:ok, pid} = TaskSupervisor.start_task(1) TaskSupervisor.stop_task(pid)
Agent for Simple State defmodule SimpleCounter do use Agent
def start_link(initial_value) do Agent.start_link(fn -> initial_value end, name: MODULE) end
def increment do Agent.update(MODULE, &(&1 + 1)) end
def decrement do Agent.update(MODULE, &(&1 - 1)) end
def value do Agent.get(MODULE, & &1) end
def reset do Agent.update(MODULE, fn _ -> 0 end) end end
Usage
{:ok, _pid} = SimpleCounter.start_link(0) SimpleCounter.increment() SimpleCounter.value() # => 1
Task for Async Operations defmodule DataFetcher do def fetch_all do tasks = [ Task.async(fn -> fetch_users() end), Task.async(fn -> fetch_posts() end), Task.async(fn -> fetch_comments() end) ]
results = Task.await_many(tasks, 5000)
%{
users: Enum.at(results, 0),
posts: Enum.at(results, 1),
comments: Enum.at(results, 2)
}
end
defp fetch_users do # Simulate API call Process.sleep(100) ["user1", "user2", "user3"] end
defp fetch_posts do Process.sleep(200) ["post1", "post2"] end
defp fetch_comments do Process.sleep(150) ["comment1", "comment2", "comment3"] end end
Task.Supervisor for Managed Tasks defmodule MyApp.TaskSupervisor do use Task.Supervisor
def start_link(_opts) do Task.Supervisor.start_link(name: MODULE) end
def run_task(fun) do Task.Supervisor.async(MODULE, fun) end
def run_task_nolink(fun) do Task.Supervisor.async_nolink(MODULE, fun) end end
In application.ex
children = [ {Task.Supervisor, name: MyApp.TaskSupervisor} ]
Usage
task = Task.Supervisor.async( MyApp.TaskSupervisor, fn -> expensive_operation() end ) result = Task.await(task)
GenServer with Timeouts defmodule SessionManager do use GenServer
@timeout 60_000 # 60 seconds
def start_link(session_id) do GenServer.start_link(MODULE, session_id) end
def refresh(pid) do GenServer.cast(pid, :refresh) end
@impl true def init(session_id) do {:ok, session_id, @timeout} end
@impl true def handle_cast(:refresh, state) do {:noreply, state, @timeout} end
@impl true def handle_info(:timeout, state) do IO.puts("Session #{state} timed out") {:stop, :normal, state} end end
Registry for Process Lookup defmodule UserSession do use GenServer
def start_link(user_id) do GenServer.start_link( MODULE, user_id, name: via_tuple(user_id) ) end
def via_tuple(user_id) do {:via, Registry, {MyApp.Registry, {:user_session, user_id}}} end
def send_message(user_id, message) do case Registry.lookup(MyApp.Registry, {:user_session, user_id}) do [{pid, _}] -> GenServer.cast(pid, {:message, message}) [] -> {:error, :not_found} end end
@impl true def init(user_id) do {:ok, %{user_id: user_id, messages: []}} end
@impl true def handle_cast({:message, message}, state) do {:noreply, %{state | messages: [message | state.messages]}} end end
In application.ex
children = [ {Registry, keys: :unique, name: MyApp.Registry} ]
Implementing GenServer with State Cleanup defmodule FileWatcher do use GenServer
def start_link(file_path) do GenServer.start_link(MODULE, file_path) end
@impl true def init(file_path) do case File.open(file_path, [:read]) do {:ok, file} -> schedule_check() {:ok, %{file: file, path: file_path, position: 0}} {:error, reason} -> {:stop, reason} end end
@impl true def handle_info(:check, state) do # Read new lines from file schedule_check() {:noreply, state} end
@impl true def terminate(_reason, %{file: file}) do File.close(file) :ok end
defp schedule_check do Process.send_after(self(), :check, 1000) end end
Using ETS with GenServer defmodule CacheServer do use GenServer
def start_link(_opts) do GenServer.start_link(MODULE, :ok, name: MODULE) end
def put(key, value) do GenServer.call(MODULE, {:put, key, value}) end
def get(key) do case :ets.lookup(MODULE, key) do [{^key, value}] -> {:ok, value} [] -> :not_found end end
@impl true def init(:ok) do :ets.new(MODULE, [:named_table, :set, :public]) {:ok, %{}} end
@impl true def handle_call({:put, key, value}, _from, state) do :ets.insert(MODULE, {key, value}) {:reply, :ok, state} end end
When to Use This Skill
Use elixir-otp-patterns when you need to:
Build concurrent applications with isolated processes Implement fault-tolerant systems with supervision trees Manage application state across process lifecycles Create worker pools for async task processing Build real-time systems with multiple concurrent users Implement pub/sub or event-driven architectures Create distributed systems with process communication Handle long-running background jobs Build scalable web servers and APIs Best Practices Use GenServer for stateful processes with complex logic Use Agent for simple state that doesn't need custom logic Use Task for one-off async operations Always define proper supervision strategies Use Registry for dynamic process lookup Implement proper timeout handling Clean up resources in terminate/2 callbacks Use via tuples for named process registration Separate client API from server callbacks Keep handle_* functions focused and simple Common Pitfalls Not implementing proper supervision strategies Blocking GenServer calls with long-running operations Forgetting to handle :timeout messages Not cleaning up resources in terminate/2 Using cast when you need synchronous confirmation Creating too many processes unnecessarily Not handling process exits properly Storing large data in process state instead of ETS Not using Registry for dynamic process management Ignoring backpressure in async operations Resources Elixir GenServer Guide Supervisor Documentation OTP Design Principles Elixir in Action Book Agent Guide Task Documentation