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.
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.