Use when writing OTP code in Elixir. Contains insights about GenServer bottlenecks, supervision patterns, ETS caching, and choosing between OTP abstractions that differ from typical concurrency thinking.
/plugin marketplace add georgeguimaraes/claude-code-elixir/plugin install elixir-thinking@claude-code-elixirThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Paradigm shifts for OTP design. These insights challenge typical concurrency and state management patterns.
GENSERVER IS A BOTTLENECK BY DESIGN
A GenServer processes ONE message at a time. This is intentional—it serializes access.
Before creating a GenServer, ask:
The ETS pattern: GenServer owns ETS table, writes serialize through GenServer, reads bypass it entirely.
No exceptions:
The Calculator Anti-Pattern:
# WRONG: Creates bottleneck for no reason
defmodule Calculator do
use GenServer
def add(a, b), do: GenServer.call(__MODULE__, {:add, a, b})
end
No state needed = no GenServer needed. Use def add(a, b), do: a + b.
Warning signs your GenServer is a bottleneck:
The pattern: GenServer owns ETS, writes serialize through GenServer, reads bypass it entirely.
def init(_) do
:ets.new(:cache, [:named_table, :public, read_concurrency: true])
{:ok, nil}
end
# Writes through GenServer (serialized)
def put(key, value), do: GenServer.call(__MODULE__, {:put, key, value})
# Reads bypass GenServer (concurrent!)
def get(key) do
case :ets.lookup(:cache, key) do
[{^key, value}] -> {:ok, value}
[] -> :error
end
end
:read_concurrency optimizes for concurrent reads. This is THE solution to GenServer bottlenecks.
Task.async spawns a linked process. If the task crashes, your caller crashes too.
# Task.async - linked, no supervision
task = Task.async(fn -> might_fail() end)
# If might_fail() raises, YOUR process crashes too
Task.Supervisor gives you options:
| Pattern | On task crash |
|---|---|
Task.async/1 | Caller crashes (linked, unsupervised) |
Task.Supervisor.async/2 | Caller crashes (linked, supervised) |
Task.Supervisor.async_nolink/2 | Caller survives, can handle error |
Why use Task.Supervisor.async over Task.async? (Both crash the caller)
$callers metadata in crash logsWhen Task.async is fine:
The real win is async_nolink — caller survives task failure:
# Supervision tree
{Task.Supervisor, name: MyApp.TaskSupervisor}
# Caller survives if task crashes
task = Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
might_fail()
end)
case Task.yield(task, 5000) || Task.shutdown(task) do
{:ok, result} -> {:ok, result}
{:exit, reason} -> {:error, reason}
nil -> {:error, :timeout}
end
Bottom line: Use Task.Supervisor when you need async_nolink, production-grade observability, or graceful shutdown. Use Task.async for quick experiments where crash-together is acceptable.
Makes sense—dynamic children have no ordering relationships.
Use DynamicSupervisor for:
When starting millions of children, single DynamicSupervisor becomes bottleneck:
{PartitionSupervisor, child_spec: DynamicSupervisor, name: MyApp.Supervisors}
# Routes by key hash
DynamicSupervisor.start_child(
{:via, PartitionSupervisor, {MyApp.Supervisors, user_id}},
child_spec
)
Don't create atoms dynamically. Use Registry:
defp via_tuple(id), do: {:via, Registry, {MyApp.Registry, id}}
def start_link(id) do
GenServer.start_link(__MODULE__, id, name: via_tuple(id))
end
Processes auto-unregister on death.
| Tool | Scope | Use Case |
|---|---|---|
| Registry | Single node | Named dynamic processes |
| :pg | Cluster-wide | Process groups, pub/sub |
:pg replaced :pg2 (deprecated OTP 23). It's what Phoenix.PubSub uses.
:pg.join(:my_scope, :room_123, self())
members = :pg.get_members(:my_scope, :room_123)
Standard DynamicSupervisor and Registry are node-local.
Horde provides:
Horde.DynamicSupervisor — Distributed supervisorHorde.Registry — Distributed registrySwarm is deprecated — doesn't re-register processes that restart outside handoff.
| Tool | Use For |
|---|---|
| Broadway | External queues (SQS, Kafka, RabbitMQ) |
| Oban | Background jobs with database persistence |
Broadway is NOT a job queue. It's a data ingestion pipeline with batching and backpressure.
Use gen_statem when you have explicit states + transitions:
def handle_event(:cast, :connect, :disconnected, data) do
{:next_state, :connecting, data, [{:state_timeout, 5000, :timeout}]}
end
def handle_event(:state_timeout, :timeout, :connecting, data) do
{:next_state, :disconnected, data}
end
State timeouts are first-class. Better than rolling your own with Process.send_after.
:sys.get_state(pid) # Current state
:sys.trace(pid, true) # Trace events
:sys.statistics(pid, true) # Start collecting stats
:sys.suspend(pid) # Pause processing
# CRITICAL: Turn off when done!
:sys.no_debug(pid)
Excessive debug handlers seriously damage performance.
For truly static, read-heavy data:
:persistent_term.put(:config, %{...})
:persistent_term.get(:config)
Faster reads than ETS. Data persists past crashes. Use for configuration, lookup tables.
| Need | Use |
|---|---|
| Memory cache | ETS |
| Disk persistence | DETS (2GB limit) |
| Transactions | Mnesia |
| Distribution | Mnesia |
| RAM + disk | Mnesia (configurable per table) |
| Strategy | Children Relationship |
|---|---|
| :one_for_one | Independent |
| :one_for_all | Interdependent (all restart) |
| :rest_for_one | Sequential dependency |
Think about failure cascades BEFORE coding.
Agent is GenServer under the hood.
| Use Agent | Use GenServer |
|---|---|
| Simple state (Map, counter) | Complex callbacks |
| Prototyping | Production |
| Would use Enum operations | Need handle_info, init logic |
If Agent feels clunky, extracting to GenServer is straightforward.
Need state?
├── No → Plain function
└── Yes → Complex behavior?
├── No → Agent
└── Yes → Supervision?
├── No → spawn_link
└── Yes → Request/response?
├── No → Task.Supervisor
└── Yes → Explicit states?
├── No → GenServer
└── Yes → GenStateMachine
Use Poolboy/NimblePool when:
:poolboy.transaction(:worker_pool, fn worker ->
GenServer.call(worker, :do_work)
end)
Phoenix, Ecto, and most libraries emit telemetry events. Attach handlers:
:telemetry.attach("my-handler", [:phoenix, :endpoint, :stop], &handle/4, nil)
Use Telemetry.Metrics + reporters (StatsD, Prometheus, LiveDashboard).
| Excuse | Reality |
|---|---|
| "GenServer is the Elixir way" | GenServer is ONE tool. It's a bottleneck by design. |
| "Task.async is simpler" | Fine for experiments. Use Task.Supervisor for async_nolink and production observability. |
| "I'll add ETS later if needed" | Design for load now. Retrofitting is harder. |
| "DynamicSupervisor needs strategies" | DynamicSupervisor only supports :one_for_one. That's fine. |
| "I need atoms for process names" | Registry exists. Never create atoms dynamically. |
| "Oban is overkill, I'll use Broadway" | Different tools. Oban = jobs, Broadway = external queues. |
| "I'll use :pg2 for distribution" | :pg2 is deprecated. Use :pg. |
| "Poolboy for everything" | Pools are for limited resources. Most things don't need pools. |
| "I need a process per user" | Only if you need state/concurrency/isolation per user. |
| "Agent is too simple" | Agent IS GenServer. Extract when you need callbacks. |
Any of these? Re-read The Iron Law and use the Abstraction Decision Tree.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.