12 pattern/smell files covering GenServer, process design, data transforms, error handling, testing, typespecs, documentation, behaviours, macros, modules, anti-patterns, and common mistakes. All patterns cite specific Elixir source files and line numbers.
4.8 KiB
Phoenix Deviations from Elixir Core
Where Phoenix deliberately differs from Elixir core patterns and why.
1. Heavy Macro Usage for Performance
Elixir core philosophy: Keep macro usage minimal. From the Router source:
Phoenix does its best to keep the usage of macros low.
Phoenix deviation: The Router uses macros extensively.
Source: lib/phoenix/router.ex:109-123
We use
get,post,put, anddeleteto define your routes. We use macros for two purposes:
They define the routing engine... Phoenix compiles all of your routes to a single case-statement with pattern matching rules
For each route you define, we also define metadata to implement
Phoenix.VerifiedRoutes
Why the deviation: Performance. Elixir core uses macros sparingly because they add cognitive complexity. Phoenix justifies them because routing is the hottest path in a web app — compile-time optimization yields measurable request/second gains.
2. import without Restriction in Router
Elixir core pattern: Always use import Module, only: [...] to be explicit.
Phoenix deviation: The Router imports entire modules:
Source: lib/phoenix/router.ex:274-276
import Phoenix.Router
import Plug.Conn
import Phoenix.Controller
Why the deviation: The Router is a DSL. Users need get, post, pipe_through, scope, resources, plug, fetch_session, etc. — all available without qualification. Restricting imports would make the DSL unusable.
3. Compile-Time State Accumulation
Elixir core pattern: Modules are generally stateless during compilation. Functions are defined and that's it.
Phoenix deviation: Aggressive use of module attribute accumulation.
Source: lib/phoenix/router.ex:271-280
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
@phoenix_pipeline nil
Phoenix.Router.Scope.init(__MODULE__)
@before_compile unquote(__MODULE__)
Why the deviation: The Router needs to collect ALL routes, then compile them into a single dispatch function. This requires building up state during module compilation, then consuming it all at @before_compile.
4. Channel Restart Strategy: :temporary
Elixir core GenServer default: :permanent (always restart).
Phoenix Channel default: :temporary (never restart).
Source: lib/phoenix/channel.ex:470-475
def child_spec(init_arg) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init_arg]},
shutdown: @phoenix_shutdown,
restart: :temporary
}
end
Why the deviation: A crashed channel should NOT auto-restart — the client needs to explicitly reconnect and rejoin. Auto-restarting would create a channel without a connected client, which is meaningless.
5. Auto-Hibernation
Elixir core GenServer: No default hibernation — processes stay in memory.
Phoenix Channel: Defaults to hibernate after 15 seconds of inactivity.
Source: lib/phoenix/channel.ex:460
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
def start_link(triplet) do
GenServer.start_link(Phoenix.Channel.Server, triplet,
hibernate_after: @phoenix_hibernate_after
)
end
Why the deviation: Web apps have many idle connections. Channels for users who are "connected but not active" are common. Hibernation reclaims memory for the heap without killing the process. A chat app with 10,000 connected users benefits enormously.
6. Plug.Builder vs Raw Behaviour
Elixir core: Behaviours define contracts. Implementations are manual.
Phoenix Endpoint: Uses Plug.Builder — a macro that generates the call/2 pipeline by chaining plugs at compile time.
Source: lib/phoenix/endpoint.ex:481-483
defp plug() do
quote location: :keep do
use Plug.Builder, init_mode: Phoenix.plug_init_mode()
...
end
end
Why the deviation: The Plug specification (init/1 + call/2) is too low-level for composing dozens of middleware. Plug.Builder provides the plug macro that chains them automatically. It's a higher-level abstraction over the raw behaviour pattern.
7. Exception Structs with HTTP Status Codes
Elixir core exceptions: Pure data — message, maybe some context fields.
Phoenix exceptions: Include plug_status for HTTP response mapping.
Source: lib/phoenix/router.ex:7-8
defmodule NoRouteError do
defexception plug_status: 404, message: "no route found", conn: nil, router: nil
end
defmodule MalformedURIError do
defexception [:message, plug_status: 400]
end
Why the deviation: In a web context, exceptions need to map to HTTP status codes. Plug's error handling middleware reads plug_status to determine the response code. This bridges the gap between Elixir's exception system and HTTP semantics.