Name dynamic processes with `:via` tuples

almirsarajcic

almirsarajcic

2 hours ago

0 comments

The moment you need one process per game, per room, or per upload, the naming problem shows up — and the tempting fix is String.to_atom("game_#{id}"). That builds an atom from user input, which is never garbage collected and will eventually take down the VM. It also leaves the name registered against nothing when the process dies. Registry solves both at once.

defmodule MyApp.Game do
  use GenServer

  def start_link(id), do: GenServer.start_link(__MODULE__, id, name: via(id))

  def score(id), do: GenServer.call(via(id), :score)

  defp via(id), do: {:via, Registry, {MyApp.GameRegistry, {:game, id}}}

  @impl GenServer
  def init(id), do: {:ok, %{id: id, score: 0}}

  @impl GenServer
  def handle_call(:score, _from, state), do: {:reply, state.score, state}
end

A :via tuple is accepted anywhere a process name is — GenServer.start_link/3, GenServer.call/3, GenServer.cast/2, DynamicSupervisor.start_child/2. The key can be any term, so {:game, id} works directly with no string interpolation and no atom creation.

Start the registry in your supervision tree:

children = [
  {Registry, keys: :unique, name: MyApp.GameRegistry},
  {DynamicSupervisor, name: MyApp.GameSupervisor, strategy: :one_for_one}
]

Deregistration is automatic

This is the part that makes it worth the indirection. Registry monitors every registered process and removes the entry when it exits — no cleanup callback, no stale name:

{:ok, pid} = MyApp.Game.start_link("abc")

Registry.lookup(MyApp.GameRegistry, {:game, "abc"})
# => [{#PID<0.123.0>, nil}]

GenServer.stop(pid)

Registry.lookup(MyApp.GameRegistry, {:game, "abc"})
# => []

With atom names you would have to unregister by hand, and a crash would skip that code entirely.

Start-or-find

Because registration happens inside start_link/3, a duplicate start fails cleanly with the existing pid, which is exactly what you need for lazy process creation:

def ensure_started(id) do
  case DynamicSupervisor.start_child(MyApp.GameSupervisor, {MyApp.Game, id}) do
    {:ok, pid} -> {:ok, pid}
    {:error, {:already_started, pid}} -> {:ok, pid}
    error -> error
  end
end

No lookup-then-start race — the registry decides the winner atomically.

Values, not just names

The third element of the registration key can carry metadata, and keys: :duplicate turns the same registry into a pubsub-style index:

Registry.register(MyApp.GameRegistry, {:lobby, lobby_id}, %{role: :player})

Registry.dispatch(MyApp.GameRegistry, {:lobby, lobby_id}, fn entries ->
  for {pid, %{role: :player}} <- entries, do: send(pid, :starting)
end)

The rule of thumb: atoms are for names you write in source code, Registry is for names you compute at runtime. Anything derived from an id, a slug, or user input belongs in the second category.

Registry docs

Comments (0)

Sign in with GitHub to join the discussion