DEV Community

Guilherme Bordallo
Guilherme Bordallo

Posted on

Creating Named GenServers in Elixir Using Registry

In this guide, we’ll explore how to use Registry along with a Dynamic Supervisor to create named GenServers.

Let's start by creating a simple GenServer

defmodule Server do
  use GenServer

  def start_link({init_arg, name}) do
    GenServer.start_link(__MODULE__, init_arg, name: name)
  end

  @impl true
  def init(arg) do
    {:ok, arg}
  end

  def state(name) do
    GenServer.call(name, :state)
  end

  @impl true
  def handle_call(:state, _from, state) do
    {:reply, state, state}
  end
end
Enter fullscreen mode Exit fullscreen mode
{:module, Server, <<70, 79, 82, 49, 0, 0, 19, ...>>, {:handle_call, 3}}
Enter fullscreen mode Exit fullscreen mode

Now, we can start a new instance of the GenServer and attempt to retrieve its current state.

initial_value = 123
server_name = :server
Server.start_link({initial_value, server_name})
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.160.0>}
Enter fullscreen mode Exit fullscreen mode
Server.state(:server)
Enter fullscreen mode Exit fullscreen mode
123
Enter fullscreen mode Exit fullscreen mode

Note that the name cannot be a string:

Server.start_link({123, "server"})
Enter fullscreen mode Exit fullscreen mode

Dynamic Supervisor

With our GenServer ready, the next step is to create a Dynamic Supervisor that will manage and start our GenServers.

defmodule DSupervisor do
  use DynamicSupervisor

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

  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one)
  end

  def start_server(initial_value, name) do    
    DynamicSupervisor.start_child(__MODULE__, {Server, {initial_value, name}})
  end
end

Enter fullscreen mode Exit fullscreen mode
{:module, DSupervisor, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:start_server, 2}}
Enter fullscreen mode Exit fullscreen mode

Now, we can start the Dynamic Supervisor and use it to launch another GenServer.

DSupervisor.start_link(1)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.170.0>}
Enter fullscreen mode Exit fullscreen mode
DSupervisor.start_server(321, :test)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.171.0>}
Enter fullscreen mode Exit fullscreen mode
Server.state(:server) |> IO.inspect()
Server.state(:test)
Enter fullscreen mode Exit fullscreen mode
123
Enter fullscreen mode Exit fullscreen mode
321
Enter fullscreen mode Exit fullscreen mode

So, what’s the issue?

Naming a GenServer using atoms is generally not a problem. However, if your application dynamically creates GenServers with different names—such as when users start GenServers by creating a new match in a browser game or starting a new chat room—this approach can lead to problems.

Atoms are not garbage collected, and dynamically generating them as names for GenServers increases memory usage over time. Eventually, this can exhaust the atom table and cause the application to fail.

To solve this, we need a way to associate a custom name with a GenServer while storing its PID. Fortunately, Elixir provides a built-in solution for this: the Registry.

Registry.start_link(keys: :unique, name: Registry)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.172.0>}
Enter fullscreen mode Exit fullscreen mode

Now we can use the Registry to register process names.

name = {:via, Registry, {Registry, "cool_name"}}
DSupervisor.start_server(42, name)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.174.0>}
Enter fullscreen mode Exit fullscreen mode
Server.state(name)
Enter fullscreen mode Exit fullscreen mode
42
Enter fullscreen mode Exit fullscreen mode

The Registry allows us to fetch the PID of a process using the lookup function, which we will use in this guide to terminate a server.

[{pid, _value}] = Registry.lookup(Registry, "cool_name")
 DynamicSupervisor.terminate_child(DSupervisor, pid)
Enter fullscreen mode Exit fullscreen mode
:ok
Enter fullscreen mode Exit fullscreen mode

Registry will monitor the GenServer and automatically remove the registration when the GenServer is terminated.

Server.state(name)
Enter fullscreen mode Exit fullscreen mode

By using the Registry, we can safely register and manage names for GenServers with ease, ensuring that each process is uniquely identifiable and accessible. The Registry also helps by automatically cleaning up registrations when a GenServer is terminated, preventing memory leaks and improving the efficiency of your application.

With this approach, you can build scalable, maintainable systems where users can dynamically create and interact with multiple GenServers without worrying about the limitations of atom-based naming.

A few noteworthy details

When starting the Registry or the Dynamic Supervisor, the name we use sometimes looks like a module, but they are just atoms, they could be named differently, like :registry or :supervisor

Registry.start_link(keys: :unique, name: :registry)
name = {:via, Registry, {:registry, "another_name"}}
DSupervisor.start_server(42, name)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.5230.0>}
Enter fullscreen mode Exit fullscreen mode

Interacting with the Registry could be encapsulated within the GenServer, making it easier to work with.


defp registry_name(name), do: {:via, Registry, {:registry, name}}

def start_link({init_arg, name}) do
  GenServer.start_link(__MODULE__, init_arg, name: registry_name(name))
end

def state(name) do
  GenServer.call(registry_name(name), :state)
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)