Extracted patterns, conventions, and code smells directly from the Elixir and Phoenix source code with file path and line number citations. Covers: GenServer, error handling, data transforms, process design, testing, documentation, typespecs, macros, behaviours, module organization, Phoenix-specific patterns, framework deviations, and anti-patterns.
6.0 KiB
Behaviour Patterns
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
1. Behaviour Definition with @callback
Source: lib/elixir/lib/gen_server.ex:577-812 (all callback definitions)
@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.
2. @optional_callbacks for Extensibility
Source: lib/phoenix/channel.ex:442-448
@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.
3. @behaviour Declaration in __using__
Source: lib/phoenix/channel.ex:450-453
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:836
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.
4. Default Implementations via defoverridable
Source: lib/elixir/lib/gen_server.ex:849
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.
5. Phoenix Channel: Behaviour + Process + Protocol
Source: lib/phoenix/channel.ex:364-448 (full callback set)
The Channel behaviour combines:
- Required callback:
join/3(authorization gate) - Optional callbacks:
handle_in/3,handle_info/2, etc. (event handlers) - Process semantics: Each channel is a GenServer (line 476-479)
- Configuration via module attributes:
@phoenix_log_join,@phoenix_hibernate_after
# 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.
6. Callback Documentation Pattern
Source: lib/phoenix/channel.ex:350-363 (join callback doc)
@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:
- A
@docexplaining when it's called and what it should do - A concrete example
- 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.
7. Phoenix.Endpoint: Behaviour as Interface Contract
Source: lib/phoenix/endpoint.ex:408
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.