#25: What are GenServers in Elixir?

Photo by Taylor Vick on Unsplash

#25: What are GenServers in Elixir?

·

3 min read

In a previous post, I did a deep dive into Elixir processes (see post #24) which are the lowest-level concurrency primitives in Elixir, but it’s pretty rare to use them directly. Instead, you will use libraries that let you build processes without worrying about the details.

GenServers in Elixir are a fundamental component of the OTP (Open Telecom Platform) framework and it is the most common process library written in Erlang. Elixir provides the GenServer module to seamlessly bring this Erlang library into Elixir. It’s a very important library, so it has been integrated into the language very well.

The GenServer library streamlines the development of concurrent processes by addressing key challenges:

  1. State Management:

    • GenServer facilitates the storage and accessibility of state within a process, enabling modifications in response to incoming messages.
  2. Messaging APIs:

    • By offering synchronous and asynchronous messaging functions, GenServer simplifies communication with processes using underlying mechanisms like send and receive.

    • These APIs handle intricate details such as timeouts and responses, enhancing the efficiency of message-passing operations.

GenServer incorporates best practices to mitigate risks that could arise from working directly with processes, ensuring smoother and more reliable process interactions.

Let's build a GenServer!

defmodule MyGenServer do
  use GenServer

  def announce(server \\ __MODULE__) do
    GenServer.cast(server, :announce)
  end

  def add(a, b, server \\ __MODULE__) do
    GenServer.call(server, {:add, a, b})
  end

  def start_link(init_args) do
    GenServer.start_link(__MODULE__, init_args)
  end

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

  def handle_cast(:announce, state) do
    IO.puts("Announcing something!")
    {:noreply, state}
  end

  def handle_call({:add, a, b}, _from, state) do
    result = a + b
    {:reply, result, %{state | last_result: result}}
  end
end

You can test this GenServer in an IEx session as follows:

{:ok, pid} = MyGenServer.start_link(%{last_result: nil})
# Output: {:ok, #PID<0.123.0>}

MyGenServer.add(5, 10, pid)
# Output: 15

MyGenServer.announce(pid)
# Output: Announcing something!

In this example, we define a GenServer module MyGenServer with start_link, init, handle_cast, and handle_call functions. We also added two wrapper functions add and announce that are calling the GenServer functions so that callers no longer need to concern themselves with the internals of the GenServer.

BEAM - A Paradigm shift

The traditional model of building applications, especially prevalent in the Ruby community, follows a single-stack-oriented approach. In this model, applications typically operate within a single stack or process, starting from an entry point (such as a web request or background worker) and executing code linearly to achieve their objectives. While this approach has its advantages, such as simplicity and familiarity, it poses challenges when it comes to scalability, shared data structures, and optimizing resource allocation between different components of the application.

On the other hand, the properties of BEAM processes in Elixir introduce a paradigm shift in application development. By leveraging the BEAM's process model, developers can break away from the constraints of a single-stack architecture and adopt a more modular and scalable approach. Instead of a monolithic skyscraper-like structure where all components are tightly coupled, Elixir applications can be designed as interconnected cities comprising smaller, purpose-driven subsystems.

Each subsystem in an Elixir application can have its own stack, garbage collection mechanism, and error handling processes. Despite their independence, these subsystems are deployed as part of the larger application, ensuring that complexity is managed effectively. Communication between these subsystems is facilitated through message passing, enabling seamless interaction while maintaining encapsulation and modularity.

This city-like architecture in Elixir allows developers to optimize specific parts of the application that may become performance bottlenecks, add new subsystems without disrupting existing functionality, and create a cohesive and scalable application ecosystem. The BEAM's process model plays a pivotal role in enabling this flexible and resilient architecture, empowering developers to build complex systems that are both robust and maintainable.