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.
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:
@moduledoc- Types
child_spec/__using__start_link/start(lifecycle)- Public API functions (alphabetical or logical grouping)
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:
- Task:
@enforce_keys+ minimal struct — all fields required, enforced at creation - 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.