GenServer

One of the most widely used OTP abstractions is GenServer. A GenServer (generic server) provides a standardized interface for handling sync and async requests and managing state.

defmodule Counter do
  use GenServer

  def init(initial_value) do
    {:ok, initial_value}
  end

  # sync calls
  def handle_call(:get_count, _from, state) do
    {:reply, state, state}
  end

  # async calls
  def handle_cast(:incr, state) do
    {:noreply, state + 1}
  end

  def handle_cast(:decr, state) do
    {:noreply, state - 1}
  end
end

Use the Counter:

GenServer.start_link(Counter, 0, name: Counter)
GenServer.cast(Counter, :incr)
GenServer.call(Counter, :get_count) # 1
GenServer.cast(Counter, :decr)
GenServer.call(Counter, :get_count) # 0

Add helper functions

defmodule Counter do
  use GenServer
  
  def init(initial_value) do
    {:ok, initial_value}
  end

  # sync calls
  def handle_call(:get_count, _from, state) do
    {:reply, state, state}
  end

  # async calls
  def handle_cast(:incr, state) do
    {:noreply, state + 1}
  end

  def handle_cast(:decr, state) do
    {:noreply, state - 1}
  end

  # helper functions
  def incr, do: GenServer.cast(__MODULE__, :incr)
  def decr, do: GenServer.cast(__MODULE__, :decr)
  def get_count, do: GenServer.call(__MODULE__, :get_count)

  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end
end

Now we can have better APIs:

Counter.start_link(0)
Counter.incr
Counter.get_count
Counter.decr
Counter.get_count

GenServers are a higher-level abstraction than processes and can be used with Supervisors.