Files
elixir-patterns/phoenix/deviations.md
T
Aaron Weiker 4ea9a884aa docs: idiomatic Elixir and Phoenix patterns with source citations
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.
2026-04-29 22:50:12 -07:00

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, and delete to 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.