# OTP Patterns Reference

## GenServer

### Full Template

```elixir
defmodule MyApp.Cache do
  @moduledoc "Simple ETS-backed cache with TTL."
  use GenServer

  require Logger

  # -- Client API --

  def start_link(opts \\ []) do
    name = Keyword.get(opts, :name, __MODULE__)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  @spec get(term()) :: {:ok, term()} | :miss
  def get(key), do: GenServer.call(__MODULE__, {:get, key})

  @spec put(term(), term(), pos_integer()) :: :ok
  def put(key, value, ttl_ms \\ 300_000), do: GenServer.cast(__MODULE__, {:put, key, value, ttl_ms})

  @spec delete(term()) :: :ok
  def delete(key), do: GenServer.cast(__MODULE__, {:delete, key})

  # -- Server Callbacks --

  @impl true
  def init(opts) do
    table = :ets.new(__MODULE__, [:set, :protected])
    interval = Keyword.get(opts, :sweep_interval, 60_000)
    schedule_sweep(interval)
    {:ok, %{table: table, sweep_interval: interval}}
  end

  @impl true
  def handle_call({:get, key}, _from, %{table: table} = state) do
    result =
      case :ets.lookup(table, key) do
        [{^key, value, expires_at}] ->
          if System.monotonic_time(:millisecond) < expires_at,
            do: {:ok, value},
            else: :miss
        [] -> :miss
      end
    {:reply, result, state}
  end

  @impl true
  def handle_cast({:put, key, value, ttl_ms}, %{table: table} = state) do
    expires_at = System.monotonic_time(:millisecond) + ttl_ms
    :ets.insert(table, {key, value, expires_at})
    {:noreply, state}
  end

  @impl true
  def handle_cast({:delete, key}, %{table: table} = state) do
    :ets.delete(table, key)
    {:noreply, state}
  end

  @impl true
  def handle_info(:sweep, %{table: table, sweep_interval: interval} = state) do
    now = System.monotonic_time(:millisecond)
    # Delete expired entries
    :ets.select_delete(table, [{{:_, :_, :"$1"}, [{:<, :"$1", now}], [true]}])
    schedule_sweep(interval)
    {:noreply, state}
  end

  defp schedule_sweep(interval), do: Process.send_after(self(), :sweep, interval)
end
```

### GenServer Best Practices

- Keep `handle_call` fast; offload heavy work to `handle_continue` or `Task`.
- Use `handle_continue` for post-init setup: `{:ok, state, {:continue, :load_data}}`.
- Return `{:stop, reason, state}` for graceful shutdown.
- Use `@impl true` on all callbacks.
- Trap exits with `Process.flag(:trap_exit, true)` only when needed.

## Supervisor

### Static Supervisor

```elixir
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Repo,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyApp.Cache,
      {MyApp.RateLimiter, limit: 100},
      {Task.Supervisor, name: MyApp.TaskSupervisor},
      MyAppWeb.Endpoint
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

### Supervision Strategies

| Strategy | Behavior | Use When |
|----------|----------|----------|
| `:one_for_one` | Restart only failed child | Children are independent |
| `:one_for_all` | Restart all on any failure | Children are interdependent |
| `:rest_for_one` | Restart failed + those started after | Sequential dependencies |

### DynamicSupervisor

```elixir
defmodule MyApp.WorkerSupervisor do
  use DynamicSupervisor

  def start_link(init_arg) do
    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @impl true
  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one, max_children: 100)
  end

  def start_worker(args) do
    spec = {MyApp.Worker, args}
    DynamicSupervisor.start_child(__MODULE__, spec)
  end

  def stop_worker(pid) do
    DynamicSupervisor.terminate_child(__MODULE__, pid)
  end
end
```

## Agent

```elixir
defmodule MyApp.Counter do
  use Agent

  def start_link(initial \\ 0) do
    Agent.start_link(fn -> initial end, name: __MODULE__)
  end

  def value, do: Agent.get(__MODULE__, & &1)
  def increment, do: Agent.update(__MODULE__, &(&1 + 1))
  def reset, do: Agent.update(__MODULE__, fn _ -> 0 end)
end
```

**When to prefer GenServer over Agent:** When you need `handle_info` (timers, messages from other processes), complex state transitions, or multiple operations that must be atomic.

## Task

### Fire-and-forget (supervised)

```elixir
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
  MyApp.Emails.deliver_welcome(user)
end)
```

### Awaited (with timeout)

```elixir
task = Task.async(fn -> expensive_computation() end)
result = Task.await(task, 30_000)  # 30s timeout
```

### Multiple concurrent tasks

```elixir
tasks = Enum.map(urls, fn url ->
  Task.async(fn -> HTTPClient.get(url) end)
end)

results = Task.await_many(tasks, 10_000)
```

### Task.async_stream (bounded concurrency)

```elixir
items
|> Task.async_stream(&process/1, max_concurrency: 10, timeout: 30_000)
|> Enum.map(fn {:ok, result} -> result end)
```

## Registry

```elixir
# In application supervisor:
{Registry, keys: :unique, name: MyApp.Registry}

# Register a process:
Registry.register(MyApp.Registry, {:worker, worker_id}, %{})

# Look up:
case Registry.lookup(MyApp.Registry, {:worker, worker_id}) do
  [{pid, _meta}] -> {:ok, pid}
  [] -> {:error, :not_found}
end

# Use as GenServer name:
GenServer.start_link(MyWorker, args, name: {:via, Registry, {MyApp.Registry, {:worker, id}}})
```

## Process Linking and Monitoring

```elixir
# Monitor (receive :DOWN message, no crash propagation)
ref = Process.monitor(pid)
receive do
  {:DOWN, ^ref, :process, ^pid, reason} -> handle_down(reason)
end

# Link (bidirectional crash propagation)
Process.link(pid)

# Trap exits (convert EXIT signals to messages)
Process.flag(:trap_exit, true)
receive do
  {:EXIT, ^pid, reason} -> handle_exit(reason)
end
```
