π° πππππ πππ π΄πππππππ πππ πΎπππππππππ ππππππ.
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"})