Defining Elixir Behaviours

Defining Behaviours: What You’ll Learn

Welcome back. In the last lesson, you used protocols to achieve polymorphism based on data types. Today, we shift to behaviours, which define a contract that modules must implement. Behaviours help you design replaceable components (for example, multiple storage backends) and provide compile-time checks for missing functions.

Declare a Behaviour (the Contract)

defmodule Storage do
  @callback save(key :: String.t(), value :: any()) :: :ok | {:error, String.t()}
  @callback load(key :: String.t()) :: {:ok, any()} | {:error, String.t()}
  @callback delete(key :: String.t()) :: :ok | {:error, String.t()}
end

Explanation:

  • Storage defines the required functions via @callback. Any module claiming to follow this contract must provide save/2, load/1, and delete/1.
  • Type specs describe inputs and outputs. For example, load/1 must return either {:ok, value} or {:error, "reason"}.
  • The main benefit is safety. If an implementing module misses a function or uses the wrong arity, Elixir warns at compile time.

Note: Behaviours are great boundaries. You can swap implementations (e.g., in-memory vs. database) without changing the calling code.

Implement the Behaviour with a GenServer

defmodule MemoryStorage do
  @behaviour Storage
  use GenServer

  # Public API
  def start_link do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def save(key, value) do
    GenServer.call(__MODULE__, {:save, key, value})
  end

  def load(key) do
    GenServer.call(__MODULE__, {:load, key})
  end

  def delete(key) do
    GenServer.call(__MODULE__, {:delete, key})
  end

  # GenServer callbacks
  def init(state), do: {:ok, state}

  def handle_call({:save, key, value}, _from, state) do
    {:reply, :ok, Map.put(state, key, value)}
  end

  def handle_call({:load, key}, _from, state) do
    case Map.fetch(state, key) do
      {:ok, value} -> {:reply, {:ok, value}, state}
      :error -> {:reply, {:error, "Key not found"}, state}
    end
  end

  def handle_call({:delete, key}, _from, state) do
    {:reply, :ok, Map.delete(state, key)}
  end
end

Explanation:

  • @behaviour Storage opts into the contract. If you forget any callback, you get a helpful compiler warning.
  • use GenServer brings in GenServer boilerplate. We keep state in a map stored inside the process.
  • Public API:
    • start_link/0 starts the server with an empty map and registers it under its module name.
    • save/2, load/1, and delete/1 are synchronous calls that interact with the server.
  • Callbacks:
    • init/1 sets the initial state.
    • handle_call/3 handles each request and returns {:reply, reply, new_state}. We update the state immutably with Map.put/3 or Map.delete/2.
    • Missing keys return {:error, "Key not found"}, matching the behaviour’s spec.

Note: GenServer.call/2 is synchronous and serialized per process, so state updates are safe without locks. In a real app, you would usually supervise this process.

Start and Use the Storage

# Start the GenServer before making calls
{:ok, _pid} = MemoryStorage.start_link()
MemoryStorage.save("user:1", %{name: "Alice"})
IO.inspect MemoryStorage.load("user:1")
MemoryStorage.delete("user:1")
IO.inspect MemoryStorage.load("user:1")

Explanation:

  • You must start the server before calling it. We register it by name in start_link/0, so calls don’t need a PID.
  • The first load/1 returns {:ok, %{name: "Alice"}}. After delete/1, the second load/1 returns {:error, "Key not found"}.
  • This usage proves the behaviour contract and the GenServer implementation work together.

Note: Because you coded against the Storage contract, you can add another module (e.g., a persistent store) with the same callbacks and swap it without changing call sites.

Summary and Next Steps

You defined a behaviour to express a clear contract, implemented it with a GenServer, and exercised the API end to end. This approach gives you compile-time safety, clean boundaries, and easy swapping of implementations.

Ready to practice and make this pattern second nature? Let’s dive in.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal