Files
elixir-patterns/patterns/modules.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

5.4 KiB

Module Organization Patterns

How modules are structured, named, and organized in Elixir core and Phoenix.

1. One Module per Concept, Nested for Sub-Concepts

Source: lib/elixir/lib/ directory structure

gen_server.ex        — GenServer (the main module)
task.ex              — Task (the main module)
task/supervised.ex   — Task.Supervised (internal implementation)
supervisor.ex        — Supervisor (the main module)

Source: lib/phoenix/ directory structure

channel.ex           — Phoenix.Channel
channel/             — Phoenix.Channel.* submodules
  server.ex          — Phoenix.Channel.Server
router.ex            — Phoenix.Router
router/              — Phoenix.Router.* submodules
  route.ex           — Phoenix.Router.Route
  scope.ex           — Phoenix.Router.Scope
  resource.ex        — Phoenix.Router.Resource
endpoint.ex          — Phoenix.Endpoint
endpoint/            — Phoenix.Endpoint.* submodules
  supervisor.ex      — Phoenix.Endpoint.Supervisor

Why: The parent module is the public API. Submodules handle implementation details. File layout mirrors module hierarchy — Phoenix.Router.Route lives in router/route.ex.

Anti-pattern: Putting all modules in a flat directory, or nesting too deeply (more than 3 levels is usually a sign of over-engineering).


2. Public API at the Top, Private Functions at the Bottom

Source: lib/elixir/lib/agent.ex (full module structure)

defmodule Agent do
  @moduledoc "..."

  # Types
  @type on_start :: ...
  @type name :: ...
  @type agent :: ...
  @type state :: ...

  # child_spec (for supervisors)
  def child_spec(arg) do ... end

  # __using__ macro
  defmacro __using__(opts) do ... end

  # Public API
  def start_link(fun, options \\ []) do ... end
  def start(fun, options \\ []) do ... end
  def get(agent, fun, timeout \\ 5000) do ... end
  def get_and_update(agent, fun, timeout \\ 5000) do ... end
  def update(agent, fun, timeout \\ 5000) do ... end
  def cast(agent, fun) do ... end
  def stop(agent, reason \\ :normal, timeout \\ :infinity) do ... end
end

The order:

  1. @moduledoc
  2. Types
  3. child_spec / __using__
  4. start_link / start (lifecycle)
  5. Public API functions (alphabetical or logical grouping)
  6. stop (lifecycle end)

Anti-pattern: Mixing private helpers between public functions, or putting start_link at the bottom where supervisors have to hunt for it.


3. @moduledoc false for Internal Modules

Source: lib/phoenix/router/route.ex:5-7

# This module defines the Route struct that is used
# throughout Phoenix's router. This struct is private
# as it contains internal routing information.
@moduledoc false

Why: Internal modules that exist for code organization but aren't part of the public API get @moduledoc false. They won't appear in generated documentation.

Anti-pattern: Documenting internal modules and confusing users about what's public API vs implementation detail.


4. Struct Definition Conventions

Source: lib/elixir/lib/task.ex:279-296

@enforce_keys [:mfa, :owner, :pid, :ref]
defstruct @enforce_keys

@type t :: %__MODULE__{
        mfa: mfa(),
        owner: pid(),
        pid: pid() | nil,
        ref: ref()
      }

Source: lib/phoenix/router/route.ex:30-46

defstruct [
  :verb,
  :line,
  :kind,
  :path,
  :hosts,
  :plug,
  :plug_opts,
  :helper,
  :private,
  :pipe_through,
  :assigns,
  :metadata,
  :trailing_slash?,
  :warn_on_verify?
]

@type t :: %Route{}

Two patterns:

  1. Task: @enforce_keys + minimal struct — all fields required, enforced at creation
  2. Route: All optional fields listed — flexible construction

Why: Use @enforce_keys when a struct is meaningless without certain fields. Omit it for structs built incrementally.

Anti-pattern: Never using @enforce_keys — allows creating invalid structs that crash later when a required field is nil.


5. Selective Imports in __using__

Source: lib/phoenix/channel.ex:463-464

import unquote(__MODULE__)
import Phoenix.Socket, only: [assign: 3, assign: 2]

Source: lib/phoenix/router.ex:271-275

import Phoenix.Router
import Plug.Conn
import Phoenix.Controller

Why: The use macro sets up the module's environment — importing the functions you'll need. Phoenix.Channel imports assign from Socket because channels work with sockets constantly.

Anti-pattern: Importing everything without restriction — namespace pollution and hard-to-trace function origins.


6. Alias at Module Scope for Readability

Source: lib/phoenix/router.ex:268

alias Phoenix.Router.{Resource, Scope, Route, Helpers}

Why: Multi-alias reduces repetition and groups related modules. The curly-brace syntax makes it clear these all share a parent namespace.

Anti-pattern: Using full module paths everywhere (Phoenix.Router.Resource.new(...)) — verbose and hard to read.


7. Boolean-Suffixed Fields in Structs

Source: lib/phoenix/router/route.ex:43-44

:trailing_slash?,
:warn_on_verify?

Why: The ? suffix on struct fields mirrors the Elixir convention for boolean-returning functions. It makes the field's type obvious without checking the typespec.

Anti-pattern: Using bare names like :trailing_slash or :has_trailing_slash — the ? convention is more idiomatic and self-documenting.