# Behaviour Patterns How behaviours are designed, implemented, and used in Elixir core and Phoenix. ## Contents 1. [Behaviour Definition with `@callback`](#1-behaviour-definition-with-callback) 2. [`@optional_callbacks` for Extensibility](#2-optional_callbacks-for-extensibility) 3. [`@behaviour` Declaration in `__using__`](#3-behaviour-declaration-in-__using__) 4. [Default Implementations via `defoverridable`](#4-default-implementations-via-defoverridable) 5. [Phoenix Channel: Behaviour + Process + Protocol](#5-phoenix-channel-behaviour--process--protocol) 6. [Callback Documentation Pattern](#6-callback-documentation-pattern) 7. [Phoenix.Endpoint: Behaviour as Interface Contract](#7-phoenixendpoint-behaviour-as-interface-contract) --- ## 1. Behaviour Definition with `@callback` **Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577) (all callback definitions) ```elixir @callback init(init_arg :: term) :: {:ok, state} | {:ok, state, timeout | :hibernate | {:continue, continue_arg}} | :ignore | {:stop, reason :: any} @callback handle_call(request :: term, from, state :: term) :: {:reply, reply, new_state} | {:noreply, new_state} | {:stop, reason, reply, new_state} | {:stop, reason, new_state} when reply: term, new_state: term, reason: term ``` **Why:** Callbacks with full type unions document every valid return. Named parameters (`init_arg`, `request`, `state`) serve as documentation. The `when` clause defines type variables used across the union. **Anti-pattern:** Defining callbacks with `@callback handle_call(term, term, term) :: term` — provides zero guidance to implementors. ### When to Use **Triggers:** - You're defining a contract that multiple modules will implement differently - You want compile-time guarantees that implementors provide required functions - The return type has multiple valid shapes that implementors must choose between **Example — before:** ```elixir # No formal contract — just "convention" in a README defmodule MyApp.PaymentGateway do def charge(amount, card), do: raise "implement me" end ``` **Example — after:** ```elixir defmodule MyApp.PaymentGateway do @callback charge(amount :: pos_integer(), card :: card_token()) :: {:ok, transaction_id :: String.t()} | {:declined, reason :: String.t()} | {:error, :network_timeout | :invalid_card} when card_token: String.t() end ``` ### When NOT to Use **Don't use this when:** - There's only one implementation and no plans for more (just use a module) - The "contract" is so simple it's a single function with one return type — a protocol or simple module works better - You need runtime dispatch based on data type (use protocols instead) **Over-application example:** ```elixir defmodule MyApp.Config do @callback get(key :: atom()) :: term() @callback put(key :: atom(), value :: term()) :: :ok end # Only ever one implementation: defmodule MyApp.Config.Env do @behaviour MyApp.Config def get(key), do: Application.get_env(:my_app, key) def put(key, value), do: Application.put_env(:my_app, key, value) end ``` **Better alternative:** ```elixir # Just a module — no behaviour ceremony needed for a singleton defmodule MyApp.Config do def get(key), do: Application.get_env(:my_app, key) def put(key, value), do: Application.put_env(:my_app, key, value) end ``` **Why:** Behaviours add value through polymorphism — multiple implementations behind one contract. A behaviour with exactly one implementation is indirection without benefit. Add the behaviour when the second implementation arrives. --- ## 2. `@optional_callbacks` for Extensibility **Source:** [lib/phoenix/channel.ex#L442](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L442) ```elixir @optional_callbacks handle_in: 3, handle_out: 3, handle_info: 2, handle_call: 3, handle_cast: 2, code_change: 3, terminate: 2 ``` **Why:** Only `join/3` is required for a Channel. Everything else has sensible defaults. This keeps the minimum implementation surface small — a Channel that just joins and broadcasts needs only one function. **Anti-pattern:** Making all callbacks required when most have reasonable defaults — forces implementors to write boilerplate they don't need. ### When to Use **Triggers:** - Your behaviour has callbacks where most implementors will use a default (e.g., `terminate/2`, `code_change/3`) - You want a minimal "get started" experience — implement one function, everything else works - Some callbacks are only needed for advanced use cases **Example — before:** ```elixir defmodule MyApp.Worker do @callback init(args :: term()) :: {:ok, state :: term()} @callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()} @callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()} @callback on_shutdown(reason :: term(), state :: term()) :: :ok end # Implementors MUST define all 4, even if on_error and on_shutdown are no-ops ``` **Example — after:** ```elixir defmodule MyApp.Worker do @callback init(args :: term()) :: {:ok, state :: term()} @callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()} @callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()} @callback on_shutdown(reason :: term(), state :: term()) :: :ok @optional_callbacks on_error: 2, on_shutdown: 2 end ``` ### When NOT to Use **Don't use this when:** - The callback is essential to correctness (if skipping it would break the system, it's required) - Every implementor WILL need to customize the behavior (making it optional hides a real requirement) - You have only 1-2 callbacks total — if they're all optional, why is it a behaviour? **Over-application example:** ```elixir defmodule MyApp.Serializer do @callback encode(term()) :: {:ok, binary()} | {:error, term()} @callback decode(binary()) :: {:ok, term()} | {:error, term()} @optional_callbacks encode: 1, decode: 1 # Both optional?! end ``` **Better alternative:** ```elixir defmodule MyApp.Serializer do @callback encode(term()) :: {:ok, binary()} | {:error, term()} @callback decode(binary()) :: {:ok, term()} | {:error, term()} # Both are required — a serializer that can't encode or decode isn't a serializer end ``` **Why:** If ALL callbacks are optional, the behaviour provides no compile-time guarantees. At least one callback should be required to justify the behaviour's existence. Optional callbacks are for extensions, not the core contract. --- ## 3. `@behaviour` Declaration in `__using__` **Source:** [lib/phoenix/channel.ex#L450](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L450) ```elixir defmacro __using__(opts \\ []) do quote do opts = unquote(opts) @behaviour unquote(__MODULE__) @on_definition unquote(__MODULE__) @before_compile unquote(__MODULE__) ... end end ``` **Source:** [lib/elixir/lib/gen_server.ex#L836](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L836) ```elixir quote location: :keep, bind_quoted: [opts: opts] do @behaviour GenServer ... end ``` **Why:** Setting `@behaviour` inside `use` means users get compile-time warnings about missing callbacks automatically. They don't need to know about the behaviour mechanism — `use Phoenix.Channel` handles it. **Anti-pattern:** Requiring users to manually add both `use MyModule` AND `@behaviour MyModule`. ### When to Use **Triggers:** - Your behaviour requires boilerplate that every implementor would write identically - You want `use MyBehaviour` to "just work" with compile-time callback verification - The behaviour has associated module attributes, process setup, or struct definitions **Example — before:** ```elixir # Users must remember both steps defmodule MyWorker do @behaviour MyApp.Worker # Easy to forget @behaviour and lose compile-time checks def init(_), do: {:ok, %{}} end ``` **Example — after:** ```elixir defmodule MyApp.Worker do defmacro __using__(_opts) do quote do @behaviour MyApp.Worker # Compile-time checks are automatic end end end defmodule MyWorker do use MyApp.Worker def init(_), do: {:ok, %{}} end ``` ### When NOT to Use **Don't use this when:** - The behaviour has no associated boilerplate — a bare `@behaviour` declaration is sufficient - You're creating a `use` macro that only sets `@behaviour` and nothing else (unnecessary indirection) - The module being "used" doesn't define callbacks (it's a utility, not a behaviour) **Over-application example:** ```elixir defmodule MyApp.Formatter do @callback format(term()) :: String.t() defmacro __using__(_opts) do quote do @behaviour MyApp.Formatter # That's it — nothing else generated end end end ``` **Better alternative:** ```elixir defmodule MyApp.Formatter do @callback format(term()) :: String.t() end # Users just add @behaviour directly — simpler, more explicit defmodule HtmlFormatter do @behaviour MyApp.Formatter def format(data), do: ... end ``` **Why:** `use` implies "this macro generates code for you." If it only sets `@behaviour`, the indirection hides what's happening without saving any work. Use `use` when there's actual code generation; use bare `@behaviour` when there isn't. --- ## 4. Default Implementations via `defoverridable` **Source:** [lib/elixir/lib/gen_server.ex#L849](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L849) ```elixir def child_spec(init_arg) do default = %{ id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]} } Supervisor.child_spec(default, unquote(Macro.escape(opts))) end defoverridable child_spec: 1 ``` **Why:** `defoverridable` provides a working default that users CAN customize but don't HAVE to. The generated function works for the 90% case. The 10% can override it. **Anti-pattern:** Not using `defoverridable` — users who need custom behavior must bypass the `use` macro entirely. ### When to Use **Triggers:** - The 90% case has an obvious default implementation (e.g., `child_spec/1`, `terminate/2`) - You want users to opt-in to customization rather than requiring it - The default is non-trivial enough that users shouldn't have to copy-paste it **Example — before:** ```elixir defmacro __using__(_opts) do quote do @behaviour MyApp.Plugin # No default — every plugin must implement format_output/1 # even though 90% just want to call inspect() end end ``` **Example — after:** ```elixir defmacro __using__(_opts) do quote do @behaviour MyApp.Plugin def format_output(data), do: inspect(data, pretty: true) defoverridable format_output: 1 end end ``` ### When NOT to Use **Don't use this when:** - The default implementation would be wrong for most cases (forces users to override = same as required) - The function is the core purpose of the behaviour (e.g., `handle_call` in GenServer has no default because the POINT is to define it) - You're providing a default that silently swallows errors or does nothing **Over-application example:** ```elixir defmacro __using__(_opts) do quote do @behaviour MyApp.EventHandler def handle_event(_event, state), do: {:ok, state} defoverridable handle_event: 2 end end # Now users can "implement" the behaviour without handling ANY events # Bugs hide because unhandled events silently succeed ``` **Better alternative:** ```elixir defmacro __using__(_opts) do quote do @behaviour MyApp.EventHandler # handle_event/2 is required — no default # If you don't handle events, you don't need this behaviour end end ``` **Why:** A default implementation that does nothing for the CORE callback creates a pit of failure — modules compile cleanly while silently dropping events. Defaults should be for auxiliary concerns (logging, shutdown, serialization), not the primary contract. --- ## 5. Phoenix Channel: Behaviour + Process + Protocol **Source:** [lib/phoenix/channel.ex#L364](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L364) (full callback set) The Channel behaviour combines: 1. **Required callback:** `join/3` (authorization gate) 2. **Optional callbacks:** `handle_in/3`, `handle_info/2`, etc. (event handlers) 3. **Process semantics:** Each channel is a GenServer (line 476-479) 4. **Configuration via module attributes:** `@phoenix_log_join`, `@phoenix_hibernate_after` ```elixir # From __using__ — configures the process @phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000) @phoenix_shutdown Keyword.get(opts, :shutdown, 5000) def child_spec(init_arg) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}, shutdown: @phoenix_shutdown, restart: :temporary } end def start_link(triplet) do GenServer.start_link(Phoenix.Channel.Server, triplet, hibernate_after: @phoenix_hibernate_after ) end ``` **Why:** The Channel behaviour demonstrates layering — it's a behaviour (compile-time contract), a process (runtime entity), and configurable (via options to `use`). Each concern is handled by the appropriate mechanism. **Anti-pattern:** Trying to encode runtime configuration in the behaviour contract itself, or conflating compile-time and runtime concerns. ### When to Use **Triggers:** - Your behaviour involves a running process (GenServer, Agent, Task-like) - The module needs both compile-time contracts AND runtime configuration - Different options affect process lifecycle (timeouts, shutdown strategies, restart policies) **Example — before:** ```elixir defmodule MyApp.Worker do @callback handle_job(job :: term()) :: :ok | {:error, term()} # No process semantics, no configuration — just a callback end ``` **Example — after:** ```elixir defmodule MyApp.Worker do @callback init(args :: term()) :: {:ok, state :: term()} @callback handle_job(job :: term(), state :: term()) :: {:ok, state :: term()} | {:error, term(), state :: term()} defmacro __using__(opts) do quote do @behaviour MyApp.Worker @worker_timeout Keyword.get(unquote(opts), :timeout, 30_000) @worker_max_retries Keyword.get(unquote(opts), :max_retries, 3) def child_spec(init_arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}, restart: :transient} end defoverridable child_spec: 1 end end end ``` ### When NOT to Use **Don't use this when:** - The behaviour is purely functional (no process, no state) — keep it simple - You're conflating too many concerns (behaviour + process + config + routing) in one module - The "configuration" is better handled at runtime via application config rather than compile-time module attributes **Over-application example:** ```elixir defmodule MyApp.Formatter do @callback format(term()) :: String.t() defmacro __using__(opts) do quote do @behaviour MyApp.Formatter @formatter_timeout Keyword.get(unquote(opts), :timeout, 5000) def child_spec(_) do %{id: __MODULE__, start: {__MODULE__, :start_link, []}} end def start_link do GenServer.start_link(__MODULE__, []) end end end end # A formatter doesn't need to be a process! ``` **Better alternative:** ```elixir defmodule MyApp.Formatter do @callback format(term()) :: String.t() end # Pure behaviour — implementors are just modules with a format/1 function # No process needed for a synchronous data transformation ``` **Why:** Not everything needs to be a process. Adding GenServer semantics to a behaviour that does synchronous data transformation is over-engineering. Reserve process+behaviour combinations for things that genuinely need state, concurrency, or lifecycle management. --- ## 6. Callback Documentation Pattern **Source:** [lib/phoenix/channel.ex#L350](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L350) (join callback doc) ```elixir @doc """ Handle channel joins by `topic`. ... ## Example def join("room:lobby", payload, socket) do if authorized?(payload) do {:ok, socket} else {:error, %{reason: "unauthorized"}} end end """ @callback join(topic :: binary, payload :: payload, socket :: Socket.t()) :: {:ok, Socket.t()} | {:ok, reply :: payload, Socket.t()} | {:error, reason :: map} ``` **Why:** Every callback gets: 1. A `@doc` explaining when it's called and what it should do 2. A concrete example 3. The full type spec with all valid returns This trio (doc + example + spec) gives implementors everything they need. **Anti-pattern:** Defining callbacks without documentation — implementors have to read source code to understand when callbacks fire. ### When to Use **Triggers:** - You're defining a behaviour that users must implement - Each callback has non-obvious semantics (when it fires, what params mean, what returns cause) - The behaviour is public and will be implemented by people who didn't write it **Example — before:** ```elixir @callback on_connect(params :: map(), state :: term()) :: {:ok, term()} | {:error, term()} ``` **Example — after:** ```elixir @doc """ Called when a client establishes a new connection. `params` contains the query parameters from the connection URL. `state` is initialized to the value returned by the transport's init. Return `{:ok, state}` to accept the connection with updated state. Return `{:error, reason}` to reject — `reason` is sent to the client as the close frame payload. ## Example def on_connect(%{"token" => token}, state) do case verify_token(token) do {:ok, user_id} -> {:ok, Map.put(state, :user_id, user_id)} :error -> {:error, :unauthorized} end end """ @callback on_connect(params :: map(), state :: term()) :: {:ok, state :: term()} | {:error, reason :: term()} ``` ### When NOT to Use **Don't use this when:** - The callback is internal and only your own code implements it - The callback name and spec are completely self-explanatory (`@callback format(String.t()) :: String.t()`) - You're writing a one-off behaviour for test mocking — extensive docs are wasted effort **Over-application example:** ```elixir @doc """ Converts the value to a string. ## Parameters - `value` — the value to convert (term) ## Returns - `String.t()` — the converted string ## Examples def to_string(123), do: "123" def to_string(:hello), do: "hello" ## Notes This callback is required. """ @callback to_string(value :: term()) :: String.t() ``` **Better alternative:** ```elixir @doc "Converts `value` to its string representation for display." @callback to_string(value :: term()) :: String.t() ``` **Why:** Documentation depth should match callback complexity. A single-purpose callback with one obvious return type needs one sentence, not a full reference page. Save detailed docs for callbacks with multiple return shapes and non-obvious triggering conditions. --- ## 7. Phoenix.Endpoint: Behaviour as Interface Contract **Source:** [lib/phoenix/endpoint.ex#L408](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/endpoint.ex#L408) ```elixir defmacro __using__(opts) do quote do @behaviour Phoenix.Endpoint unquote(config(opts)) unquote(pubsub()) unquote(plug()) unquote(server()) end end ``` **Why:** The Endpoint uses `@behaviour` to define what an endpoint MUST provide (like `config/2`), then `__using__` generates the common implementation. The behaviour is the interface; the macro provides the default implementation. **Anti-pattern:** Using only a behaviour without a `use` macro when significant boilerplate is required — forces every implementor to write the same code. ### When to Use **Triggers:** - Your behaviour defines a contract AND requires significant generated code - The "interface" is simple but the implementation wiring is complex (plugs, routing, supervision) - Users of the behaviour shouldn't need to understand the plumbing — just implement callbacks **Example — before:** ```elixir # User has to wire everything manually defmodule MyEndpoint do @behaviour Phoenix.Endpoint use Plug.Builder # ... 50 lines of boilerplate # Easy to get wrong end ``` **Example — after:** ```elixir defmodule MyEndpoint do use Phoenix.Endpoint, otp_app: :my_app # All wiring generated — just configure and add plugs plug MyAppWeb.Router end ``` ### When NOT to Use **Don't use this when:** - The generated code is minimal (just `@behaviour` — see pattern #3) - The magic is hard to debug when things go wrong (transparency > convenience) - Users need to understand what's generated to use the module correctly **Over-application example:** ```elixir defmodule MyApp.Validator do defmacro __using__(_opts) do quote do @behaviour MyApp.Validator import MyApp.Validator.DSL Module.register_attribute(__MODULE__, :validations, accumulate: true) @before_compile MyApp.Validator # 40 lines of generated code for "validation framework" # Users need a PhD in macros to debug validation errors end end end ``` **Better alternative:** ```elixir defmodule MyApp.Validator do @callback validate(term()) :: :ok | {:error, [String.t()]} end # Simple behaviour — implementors write plain Elixir defmodule UserValidator do @behaviour MyApp.Validator def validate(%{name: name}) when byte_size(name) > 0, do: :ok def validate(_), do: {:error, ["name is required"]} end ``` **Why:** The more code a `use` macro generates, the harder it is to debug. If users regularly need to read the generated code to understand failures, the abstraction is leaking. Reserve heavy `use` macros for well-established patterns (GenServer, Endpoint, Channel) where the community has internalized the mental model. ## Decision Tree - If you need a contract that multiple modules will implement differently → define a behaviour with `@callback` (Pattern 1) - If most implementors will use a default for some callbacks → mark those `@optional_callbacks` (Pattern 2) - If your behaviour requires boilerplate setup (module attributes, compile hooks) → inject `@behaviour` inside `__using__` (Pattern 3) - If 90% of implementors want the same default for a callback → provide a `defoverridable` implementation (Pattern 4) - If the behaviour involves a running process with lifecycle configuration → combine behaviour + process + module attributes (Pattern 5) - If callback semantics are non-obvious (multiple return shapes, triggering conditions) → write comprehensive `@doc` with examples on each `@callback` (Pattern 6) - If the behaviour requires significant generated boilerplate (plugs, routing, supervision wiring) → use the `use` macro as the full interface contract (Pattern 7) - If there is only one implementation and no plans for more → skip the behaviour, use a plain module