Clivern

𝙰 πš‚πš˜πšπšπš πšŠπš›πšŽ π™΄πš—πšπš’πš—πšŽπšŽπš› πšŠπš—πš π™ΎπšŒπšŒπšŠπšœπš’πš˜πš—πšŠπš• πš†πš›πš’πšπšŽπš›.

Understanding Elixir GenServer

16 August 2025

A GenServer is just a process like any other Elixir process - you can use it to store state, run code asynchronously, and much more. Think of it as having lots of tiny workers, each with their own mailbox and private desk.

Let’s jump right into a code example and then look at the available callbacks. Say we want to build a service with a GenServer that acts like a hashmap:

defmodule Hashmap do
  use GenServer

  def start_link(inital_state \\ %{}) do
    GenServer.start_link(__MODULE__, inital_state, name: __MODULE__)
  end

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

  @impl true
  def handle_call({:get, key}, _from, state) do
    value = Map.get(state, key)
    {:reply, value, state}
  end

  @impl true
  def handle_call({:get, key, default}, _from, state) do
    value = Map.get(state, key, default)
    {:reply, value, state}
  end

  @impl true
  def handle_call({:get_all}, _from, state) do
    {:reply, state, state}
  end

  @impl true
  def handle_call({:update, key, value}, _from, state) do
    new_state = Map.put(state, key, value)
    {:reply, :ok, new_state}
  end

  @impl true
  def handle_call({:put, key, value}, _from, state) do
    new_state = Map.put(state, key, value)
    {:reply, :ok, new_state}
  end

  @impl true
  def handle_cast({:delete, key}, state) do
    new_state = Map.delete(state, key)
    {:noreply, new_state}
  end
end

Now let’s try it out in elixir:

# Create a new hashmap
{:ok, pid} = Hashmap.start_link(%{"name" => "John", "age" => 30})

# Get value of key equal name
GenServer.call(pid, {:get, "name"})

# Update the name
GenServer.call(pid, {:update, "name", "Doe"})

# Add the gender
GenServer.call(pid, {:put, "gender", "Male"})

# Get all hashmap
GenServer.call(pid, {:get_all})

# Delete the gender
GenServer.cast(pid, {:delete, "gender"})

In our example above, we’re using both handle_call/3 and handle_cast/2. Here’s what makes them different:

Synchronous Calls (GenServer.call/3)

  • Client waits for response
  • Use handle_call/3 to handle
  • Good for: getting data, calculations, validation

Asynchronous Calls (GenServer.cast/2)

  • Client doesn’t wait
  • Use handle_cast/2 to handle
  • Good for: updates, notifications, fire-and-forget operations

How to Supervise in Phoenix Application

This section shows you how to create a supervised GenServer service in a Phoenix application. The service works as an in-memory key-value store that sticks around for the entire lifetime of your app.

defmodule ScutiWeb.Service.Hashmap do
  use GenServer

  def start_link(inital_state \\ %{}) do
    GenServer.start_link(__MODULE__, inital_state, name: __MODULE__)
  end

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

  @impl true
  def handle_call({:get, key}, _from, state) do
    value = Map.get(state, key)
    {:reply, value, state}
  end

  @impl true
  def handle_call({:get, key, default}, _from, state) do
    value = Map.get(state, key, default)
    {:reply, value, state}
  end

  @impl true
  def handle_call({:get_all}, _from, state) do
    {:reply, state, state}
  end

  @impl true
  def handle_call({:update, key, value}, _from, state) do
    new_state = Map.put(state, key, value)
    {:reply, :ok, new_state}
  end

  @impl true
  def handle_call({:put, key, value}, _from, state) do
    new_state = Map.put(state, key, value)
    {:reply, :ok, new_state}
  end

  @impl true
  def handle_cast({:delete, key}, state) do
    new_state = Map.delete(state, key)
    {:noreply, new_state}
  end
end

This service gets added to the supervision tree in the Application module. The supervision tree makes sure that if your service ever crashes, it’ll automatically restart itself. We’re setting the initial state with some basic app metadata:

defmodule Scuti.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      ScutiWeb.Telemetry,
      Scuti.Repo,
      {Ecto.Migrator,
       repos: Application.fetch_env!(:scuti, :ecto_repos), skip: skip_migrations?()},
      {DNSCluster, query: Application.get_env(:scuti, :dns_cluster_query) || :ignore},
      {Phoenix.PubSub, name: Scuti.PubSub},
      # Start a worker by calling: Scuti.Worker.start_link(arg)
      # {Scuti.Worker, arg},
      # Start to serve requests, typically the last entry
      ScutiWeb.Endpoint,
      {ScutiWeb.Service.Hashmap, %{
        "app_name" => "Scuti",
        "version" => "1.0.0",
        "started_at" => DateTime.utc_now()
      }}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: ScutiWeb.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  @impl true
  def config_change(changed, _new, removed) do
    ScutiWeb.Endpoint.config_change(changed, removed)
    :ok
  end

  defp skip_migrations?() do
    # By default, sqlite migrations are run when using a release
    System.get_env("RELEASE_NAME") == nil
  end
end

To use the Hashmap GenServer from anywhere else in your Phoenix app, you can talk to it directly since it’s registered under the module name. Here’s how you’d do it:

pid = ScutiWeb.Service.Hashmap
state = GenServer.call(pid, {:get_all})
IO.inspect(state)
pid = ScutiWeb.Service.Hashmap
GenServer.call(pid, {:update, "gender", "male"})