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.
This commit is contained in:
Aaron Weiker
2026-04-29 22:50:12 -07:00
commit 4ea9a884aa
16 changed files with 4857 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
# 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)
```elixir
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`
```elixir
# 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`
```elixir
@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`
```elixir
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`
```elixir
import unquote(__MODULE__)
import Phoenix.Socket, only: [assign: 3, assign: 2]
```
**Source:** `lib/phoenix/router.ex:271-275`
```elixir
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`
```elixir
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`
```elixir
: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.