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:
GenServer.call/3)handle_call/3 to handleGenServer.cast/2)handle_cast/2 to handleThis 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"})