# 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:106-128` > 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. ### When to Use **Triggers:** - You are writing a library where the hot path is called millions of times - Compile-time knowledge can eliminate runtime branching entirely - The macro generates a known, bounded set of functions (not arbitrary code) **Example — before:** ```elixir # Runtime dispatch — works but slow on hot paths defmodule MyApp.Permissions do @permissions [:read, :write, :delete, :admin] def check(user, permission) do if permission in @permissions do MapSet.member?(user.permissions, permission) else raise "Unknown permission: #{permission}" end end end ``` **Example — after:** ```elixir # Compile-time generated pattern-match functions — O(1) dispatch defmodule MyApp.Permissions do @permissions [:read, :write, :delete, :admin] for perm <- @permissions do def check(user, unquote(perm)) do MapSet.member?(user.permissions, unquote(perm)) end end def check(_user, unknown), do: raise "Unknown permission: #{unknown}" end ``` ### When NOT to Use **Don't use this when:** The performance gain is negligible (cold paths, admin screens), or when the same result can be achieved with pattern matching or maps. **Over-application example:** ```elixir # Bad: Macros for a configuration screen hit once per admin session defmodule MyApp.Admin.Settings do # Generating functions for 3 settings that change once a month for {key, default} <- [theme: "light", locale: "en", tz: "UTC"] do defmacro unquote(:"get_#{key}")(user) do # Over-engineered — this is called once when an admin loads settings quote do: Map.get(unquote(user).settings, unquote(unquote(key)), unquote(unquote(default))) end end end ``` **Better alternative:** ```elixir # Plain function — clear, testable, fast enough for cold paths defmodule MyApp.Admin.Settings do @defaults %{theme: "light", locale: "en", tz: "UTC"} def get(user, key) when is_map_key(@defaults, key) do Map.get(user.settings, key, @defaults[key]) end end ``` **Why:** Macros add compile-time complexity, make stack traces harder to read, and complicate debugging. They're justified only when the performance difference matters at scale (thousands+ calls/second). --- ## 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:303-306` ```elixir import Phoenix.Router # TODO v2: No longer automatically import dependencies 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. ### When to Use **Triggers:** - You are building a DSL where users need many functions from your module - The imported functions are the primary API (not helpers leaking into scope) - Qualified calls would make the DSL unreadable **Example — before:** ```elixir # With restricted imports — DSL becomes unreadable defmodule MyAppWeb.Router do import Phoenix.Router, only: [get: 3, post: 3, scope: 2, pipe_through: 1, pipeline: 2, plug: 1, plug: 2, resources: 3] import Plug.Conn, only: [fetch_session: 2, put_secure_browser_headers: 2] Phoenix.Router.pipeline :browser do Phoenix.Router.plug :fetch_session # ... end end ``` **Example — after:** ```elixir # Full import — DSL reads naturally defmodule MyAppWeb.Router do use Phoenix.Router pipeline :browser do plug :fetch_session plug :accepts, ["html"] end scope "/", MyAppWeb do pipe_through :browser get "/", PageController, :home resources "/users", UserController end end ``` ### When NOT to Use **Don't use this when:** You are importing a utility module where only 1-2 functions are needed, or in application code (not DSL contexts). **Over-application example:** ```elixir # Bad: Blanket import of a utility module in application code defmodule MyApp.Workers.DataProcessor do import Enum # pulls in 80+ functions import String # pulls in 50+ functions import Map # pulls in 30+ functions def process(data) do data |> map(&transform/1) |> filter(&valid?/1) # Which `map` is this? Enum.map or Map... confusion end end ``` **Better alternative:** ```elixir # Explicit imports in application code defmodule MyApp.Workers.DataProcessor do import Enum, only: [map: 2, filter: 2] def process(data) do data |> map(&transform/1) |> filter(&valid?/1) end end ``` **Why:** Unrestricted imports in application code create namespace collisions and make code harder to trace. The DSL exception exists because DSLs are designed to be a controlled vocabulary — application code is not. --- ## 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:297-312` ```elixir defp prelude(opts) do quote do Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true) @phoenix_helpers Keyword.get(unquote(opts), :helpers, true) import Phoenix.Router import Plug.Conn import Phoenix.Controller # Set up initial scope @phoenix_pipeline nil Phoenix.Router.Scope.init(__MODULE__) @before_compile unquote(__MODULE__) end end ``` **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`. ### When to Use **Triggers:** - You need to collect declarations across a module and process them holistically - The final output (generated function) depends on ALL accumulated items together - Individual declarations can't be compiled in isolation — they need global context **Example — before:** ```elixir # Without accumulation — each handler is independent, no holistic optimization defmodule MyApp.EventRouter do def handle("user.created", payload), do: UserHandler.created(payload) def handle("user.deleted", payload), do: UserHandler.deleted(payload) def handle("order.placed", payload), do: OrderHandler.placed(payload) # Works, but can't generate a dispatch table or validate at compile time end ``` **Example — after:** ```elixir # With accumulation — collect all events, compile into optimized dispatch defmodule MyApp.EventRouter do use MyApp.EventRouter.DSL handle "user.created", UserHandler, :created handle "user.deleted", UserHandler, :deleted handle "order.placed", OrderHandler, :placed end # The DSL accumulates handlers and generates optimized dispatch at @before_compile defmodule MyApp.EventRouter.DSL do defmacro __using__(_opts) do quote do Module.register_attribute(__MODULE__, :event_handlers, accumulate: true) import MyApp.EventRouter.DSL, only: [handle: 3] @before_compile MyApp.EventRouter.DSL end end defmacro handle(event, module, function) do quote do @event_handlers {unquote(event), unquote(module), unquote(function)} end end defmacro __before_compile__(env) do handlers = Module.get_attribute(env.module, :event_handlers) for {event, mod, fun} <- handlers do quote do def dispatch(unquote(event), payload), do: unquote(mod).unquote(fun)(payload) end end end end ``` ### When NOT to Use **Don't use this when:** Each item can be compiled independently, or when a simple map/config file would suffice. **Over-application example:** ```elixir # Bad: Using compile-time accumulation for static config defmodule MyApp.FeatureFlags do Module.register_attribute(__MODULE__, :flags, accumulate: true) @flags {:dark_mode, true} @flags {:beta_search, false} @flags {:new_checkout, true} @before_compile __MODULE__ # Over-engineered — these are just config values end ``` **Better alternative:** ```elixir # A map is simpler and more flexible (can be loaded from config/env) defmodule MyApp.FeatureFlags do @flags %{ dark_mode: true, beta_search: false, new_checkout: true } def enabled?(flag), do: Map.get(@flags, flag, false) end ``` **Why:** Compile-time accumulation adds cognitive overhead and makes the module harder to understand. If you don't need holistic processing of all items together (no optimized dispatch, no cross-item validation), a plain data structure is clearer. --- ## 4. Channel Restart Strategy: `:temporary` **Elixir core GenServer default:** `:permanent` (always restart). **Phoenix Channel default:** `:temporary` (never restart). **Source:** `lib/phoenix/channel.ex:464-472` ```elixir 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. ### When to Use **Triggers:** - The process's existence only makes sense while an external connection is active - Restarting without the original context (socket, params, auth) would be meaningless - The client has reconnection logic that will re-establish the process naturally **Example — before:** ```elixir # Bad: Default permanent restart for a session-bound process defmodule MyApp.GameSession do use GenServer # Default child_spec → restart: :permanent # If this crashes, it restarts without the player's connection — broken state def start_link({player_id, game_id}) do GenServer.start_link(__MODULE__, {player_id, game_id}) end end ``` **Example — after:** ```elixir # Correct: Temporary restart — client reconnects and re-creates defmodule MyApp.GameSession do use GenServer, restart: :temporary def start_link({player_id, game_id}) do GenServer.start_link(__MODULE__, {player_id, game_id}) end end ``` ### When NOT to Use **Don't use this when:** The process owns durable state that must survive crashes (e.g., a stateful worker processing a queue, an aggregator collecting metrics). **Over-application example:** ```elixir # Bad: Temporary restart on a process that owns critical state defmodule MyApp.InvoiceGenerator do use GenServer, restart: :temporary # If this crashes mid-generation, the work is lost forever # No client will "reconnect" to restart it def handle_cast({:generate, invoice_id}, state) do # Long-running PDF generation... result = generate_pdf(invoice_id) store_result(result) {:noreply, state} end end ``` **Better alternative:** ```elixir # Permanent restart — crash recovery is handled by OTP defmodule MyApp.InvoiceGenerator do use GenServer # restart: :permanent (default) def init(state) do # On restart, check for incomplete work and resume incomplete = Jobs.find_incomplete(:invoice_generation) {:ok, %{state | pending: incomplete}} end end ``` **Why:** `:temporary` means "if it dies, it's gone." That's correct for client-bound processes (channels, sessions) but dangerous for processes that own work. If no external actor will re-trigger the process, use `:permanent` or `:transient`. --- ## 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:459` ```elixir @phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000) ``` ```elixir 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. ### When to Use **Triggers:** - Many processes spend most of their time idle (connected but waiting) - Process count is high (thousands+) and memory pressure matters - Latency on the first message after idle is acceptable (GC overhead on wake) **Example — before:** ```elixir # Default GenServer — 10,000 idle processes each holding heap memory defmodule MyApp.UserPresence do use GenServer def start_link(user_id) do GenServer.start_link(__MODULE__, %{user_id: user_id, last_seen: DateTime.utc_now()}) end # Each idle process keeps its heap allocated even if doing nothing end ``` **Example — after:** ```elixir # With hibernation — idle processes release heap memory defmodule MyApp.UserPresence do use GenServer, hibernate_after: 15_000 def start_link(user_id) do GenServer.start_link(__MODULE__, %{user_id: user_id, last_seen: DateTime.utc_now()}, hibernate_after: 15_000 ) end # After 15s idle, heap is compacted — memory reclaimed end ``` ### When NOT to Use **Don't use this when:** The process is frequently active (messages every few seconds), or when wake-up latency is unacceptable (real-time processing pipelines). **Over-application example:** ```elixir # Bad: Hibernation on a process that receives messages every 100ms defmodule MyApp.MetricsCollector do use GenServer, hibernate_after: 5_000 # Receives telemetry events every 100ms — will never actually hibernate # The hibernate_after timer just adds overhead resetting on every message def handle_info({:telemetry, _event}, state) do {:noreply, update_metrics(state)} end end ``` **Better alternative:** ```elixir # Don't set hibernate_after on high-throughput processes defmodule MyApp.MetricsCollector do use GenServer # No hibernation — this process is always active, never idle def handle_info({:telemetry, _event}, state) do {:noreply, update_metrics(state)} end end ``` **Why:** Hibernation has a cost: waking from hibernate requires a full GC pass to reconstruct the heap. For processes that are always active, the timer reset overhead and impossible hibernation provide no benefit. --- ## 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:478-480` ```elixir 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. ### When to Use **Triggers:** - You are composing multiple plugs into a pipeline - You want the `plug` macro for declarative middleware ordering - You need automatic `init/1` caching and pipeline compilation **Example — before:** ```elixir # Manual plug chaining — error-prone, verbose defmodule MyApp.Pipeline do @behaviour Plug def init(opts), do: opts def call(conn, _opts) do conn |> Plug.RequestId.call(Plug.RequestId.init([])) |> Plug.Logger.call(Plug.Logger.init([])) |> MyApp.Auth.call(MyApp.Auth.init([])) |> MyApp.Router.call(MyApp.Router.init([])) end end ``` **Example — after:** ```elixir # Plug.Builder — declarative, compiled, correct defmodule MyApp.Pipeline do use Plug.Builder plug Plug.RequestId plug Plug.Logger plug MyApp.Auth plug MyApp.Router end ``` ### When NOT to Use **Don't use this when:** You have a single plug or need dynamic/conditional middleware selection at runtime. **Over-application example:** ```elixir # Bad: Using Plug.Builder for a single plug defmodule MyApp.JustAuth do use Plug.Builder plug MyApp.Auth # Only one plug — Builder adds no value end ``` **Better alternative:** ```elixir # Just use the plug directly plug MyApp.Auth # In your router or endpoint ``` **Why:** `Plug.Builder` generates pipeline compilation machinery. For a single plug, it's overhead with no benefit — just reference the plug directly where it's needed. --- ## 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:2-26` ```elixir defmodule NoRouteError do @moduledoc """ Exception raised when no route is found. """ defexception plug_status: 404, message: "no route found", conn: nil, router: nil end defmodule MalformedURIError do @moduledoc """ Exception raised when the URI is malformed on matching. """ 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. ### When to Use **Triggers:** - You are raising exceptions that should map to specific HTTP responses - You want Plug's error handling to automatically return the correct status code - Your error is domain-specific but has a clear HTTP semantic (404, 403, 422) **Example — before:** ```elixir # Generic exception — Plug returns 500 for everything defmodule MyApp.NotFoundError do defexception message: "Resource not found" end # In controller: raise MyApp.NotFoundError # → 500 Internal Server Error (wrong!) ``` **Example — after:** ```elixir # Exception with plug_status — correct HTTP mapping defmodule MyApp.NotFoundError do defexception plug_status: 404, message: "Resource not found" end # In controller: raise MyApp.NotFoundError # → 404 Not Found (correct!) ``` ### When NOT to Use **Don't use this when:** The exception is used in non-HTTP contexts (CLI tools, background workers, library code) where HTTP status codes are meaningless. **Over-application example:** ```elixir # Bad: HTTP status on a domain exception used in background jobs defmodule MyApp.PaymentDeclinedError do defexception plug_status: 402, message: "Payment declined" # This exception is also raised in async payment retry workers # where plug_status is meaningless and confusing end ``` **Better alternative:** ```elixir # Domain exception — pure, no HTTP coupling defmodule MyApp.PaymentDeclinedError do defexception [:message, :reason, :transaction_id] end # Map to HTTP at the boundary (ErrorView or fallback controller) defimpl Plug.Exception, for: MyApp.PaymentDeclinedError do def status(_exception), do: 402 def actions(_exception), do: [] end ``` **Why:** Embedding `plug_status` couples the exception to HTTP. If the exception is used in multiple contexts (web, workers, CLI), implement the `Plug.Exception` protocol separately to keep the domain exception pure and the HTTP mapping at the boundary.