As its name makes explicit, GenServers are “generic” — a common building block. They can be used to maintain state, sure, but they can also execute long-running tasks, monitor other processes, and supervise other processes and restart them when they fail. The GenServer is one important pillar of the OTP architecture. Elixir provides an abstraction atop the Erlang implementation of gen_server with its own GenServer module.

There’s a Github repository with the code for a basic poker player GenServer here, if you want to see the complete implementation. In that code, there are two important sections: the public API calls and the internal server callbacks that manage the state of the GenPlayer implementation.

The take_seat/1 function is the entry point, called when GenPlayer starts. It takes the player’s name calls the init/1 callback creating the map that is the data structure representing the poker player. The initial state is also set: the player has a name, zero chips, and no cards.

Looking at the code for the player, you’ll notice the name/1, stack/1, and cards/1 functions that give external callers information about the player. Those functions make callbacks to server-side handle_call functions which return the values requested and then set the player’s state without mutating it.

More interesting is the deal_cards/3 function.

This function calls back to a handle_cast/2 function meant to deal with making sure the player gets her cards. Imagine you’re sitting at a poker table and the dealer is flipping a card to each player, not really caring that the player received the card. By the time the player has received the card, the dealer is on to the next player. This function accepts the cards and sets the player’s state to represent her hand now contains them.

Similarly, the buy_chips/2 and bet/2 functions update the state of the player’s stack by adding and subtracting chips, respectively. The bet/2 function adds a little sugar by returning the player’s chip amount after the bet is made — this may be useful to some AI routine making decisions about that player’s next action or to some other GenServer responsible for displaying the player’s new chip count on a GUI.

The core principle to remember is that the GenServer holds the player’s state at all times, and all changes to that state go through a clearly defined public API which has an explicit set of server callbacks. And, when a function is executed the player gets an entirely new state based on the prior state. The data structure representing the player is never mutated in-place. Time moves on, and the state moves with it.

Take a moment and look over the lib/gen_player.ex code. There’s a lot of formal API/callback stuff going on. But try looking at the underlying GenServer module implementation sometime, to get an idea of how the Elixir team has abstracted away a lot of the underlying Erlang complexity. For this, we should be grateful. But they’ve given us even more….