Skip to content
Behaviours
step 1/7

Reading — step 1 of 7

Learn

~3 min readPolymorphism

Where protocols dispatch on data, behaviours dispatch on modules. A behaviour is an interface that modules can implement — Elixir's way of saying "any module that does X must have these callbacks."

Defining a behaviour

defmodule Cache do
    @callback get(key :: String.t()) :: {:ok, term()} | :error
    @callback put(key :: String.t(), value :: term()) :: :ok
    @callback delete(key :: String.t()) :: :ok
end

The @callback attribute declares the contract — function name, parameter types, return type. No implementation here.

Implementing a behaviour

defmodule MemoryCache do
    @behaviour Cache
    
    use Agent
    
    def start_link(_), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
    
    @impl true
    def get(key) do
        case Agent.get(__MODULE__, &Map.fetch(&1, key)) do
            :error -> :error
            {:ok, _} = ok -> ok
        end
    end
    
    @impl true
    def put(key, value) do
        Agent.update(__MODULE__, &Map.put(&1, key, value))
    end
    
    @impl true
    def delete(key) do
        Agent.update(__MODULE__, &Map.delete(&1, key))
    end
end
  • @behaviour Cache — declares that this module implements the Cache contract
  • @impl true — marks each function as implementing a callback

The compiler verifies all @callbacks are implemented. Missing ones get warnings.

Built-in behaviours

Elixir/OTP has many:

  • GenServer — stateful server processes
  • Supervisor — process supervision trees
  • Application — application lifecycle
  • Plug — HTTP middleware (in the Plug library)
  • Phoenix.LiveComponent — Phoenix LiveView components

Each defines a set of callbacks. Your module implements them; the framework calls them.

Optional callbacks

defmodule Cache do
    @callback get(key :: String.t()) :: {:ok, term()} | :error
    @callback put(key :: String.t(), value :: term()) :: :ok
    @callback warmup() :: :ok
    @optional_callbacks warmup: 0
end

With @optional_callbacks, modules can skip implementing those without warnings.

Behaviours vs protocols — when to use which

AspectProtocolBehaviour
Dispatch onValue typeModule
Use caseDifferent data types respond to operationsDifferent modules implement a contract
ExamplesEnumerable for List/Map/MyStructGenServer, Plug, Application
Defined withdefprotocol + defimpl@callback in module + @behaviour in implementer

Protocol example: Enum.map([1,2,3], f) — dispatches based on whether the first arg is a List, Map, etc.

Behaviour example: GenServer.start_link(MyServer, ...) — calls MyServer.init/1, MyServer.handle_call/3, etc., based on which module is passed.

Defining a custom behaviour

Useful for plugin systems, strategy patterns:

defmodule Notifier do
    @callback send(message :: String.t()) :: :ok | {:error, term()}
end

defmodule EmailNotifier do
    @behaviour Notifier
    @impl true
    def send(message), do: send_email(message)
end

defmodule SlackNotifier do
    @behaviour Notifier
    @impl true
    def send(message), do: post_to_slack(message)
end

# Caller picks at runtime:
notifier = Application.get_env(:my_app, :notifier, EmailNotifier)
notifier.send("hello")

Classic strategy pattern. Configurable, testable, swappable.

Common mistakes

  • Mixing protocol and behaviour — different mechanisms. Protocols dispatch on value; behaviours on module.
  • Forgetting @impl true — Elixir 1.5+ requires it for behaviour callbacks. Without, no warning when you mistype the function name.
  • Confusing @behavior (American) vs @behaviour (British) — Elixir uses BRITISH spelling (defbehaviour, @behaviour). American @behavior is silently wrong.
  • Implementing only some callbacks — compiler warns. Either implement all or mark missing as @optional_callbacks.
  • Behaviours where protocols would do — if you're dispatching on data, protocol. If on module, behaviour.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…