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.
11 KiB
Documentation Patterns
Patterns extracted from the Elixir standard library source code.
1. @moduledoc with Structured Sections
Source: lib/elixir/lib/gen_server.ex lines 6–100+, lib/logger/lib/logger.ex lines 6–200+
What it does: @moduledoc uses a clear hierarchical structure with ## sections covering: overview, examples, configuration, and detailed behavioral explanations. GenServer covers "Example", "Client / Server APIs", "How to supervise", "Name registration", "Timeouts", "Debugging". Logger covers "Levels", "Message", "Metadata", "Configuration" (with subsections for Boot/Compile/Runtime).
Why: Long moduledocs are effectively reference manuals. Structured sections let users jump to what they need. The pattern establishes a convention: start with "what is this" → "quick example" → "detailed topics."
Anti-pattern: Writing a single unstructured wall of text, or documenting only the happy path without covering error handling, configuration, and advanced usage.
Code example:
defmodule GenServer do
@moduledoc """
A behaviour module for implementing the server of a client-server relation.
A GenServer is a process like any other Elixir process...
## Example
The GenServer behaviour abstracts the common client-server interaction...
defmodule Stack do
use GenServer
# ...
end
## How to supervise
A GenServer is most commonly started under a supervision tree...
## Name registration
...
## Timeouts
...
## Debugging with the :sys module
...
"""
end
2. @doc with Sections and Examples
Source: lib/elixir/lib/kernel.ex lines 315–335 (abs/1), lib/logger/lib/logger.ex lines 536–540
What it does: @doc for individual functions includes: a brief one-liner, allowed-in-guards note (via @doc guard: true), explanation of edge cases, and ## Examples with doctests.
Why: Each function doc is self-contained. A developer reading docs shouldn't need to look elsewhere for basic usage. Guard-eligible functions are annotated structurally so tools can surface this.
Anti-pattern: Docs that say "see module documentation" without providing any local context. Also: examples without iex> that can't be tested automatically.
Code example:
@doc """
Returns all the available levels.
"""
@doc since: "1.16.0"
@spec levels() :: [level(), ...]
def levels(), do: @levels
3. @doc since: Version Annotation
Source: lib/logger/lib/logger.ex lines 539, 576, 813, 824, 831, lib/elixir/lib/kernel.ex line 5163+
What it does: Attaches since: "X.Y.Z" metadata to @doc indicating the Elixir version that introduced the function.
Why: Users working with multiple Elixir versions need to know API availability. ExDoc renders this prominently. The pattern is universal in the standard library for any function added after 1.0.
Anti-pattern: Adding new public functions without @since annotation, leaving version compatibility ambiguous.
Code example:
@doc since: "1.15.0"
@spec default_formatter(formatter_opts) :: {module, :logger.formatter_config()}
def default_formatter(overrides \\ []) when is_list(overrides) do
# ...
end
@doc since: "1.14.0"
def binary_slice(binary, start, size)
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
# ...
end
4. @doc guard: true Metadata
Source: lib/elixir/lib/kernel.ex lines 329, 408, 428, 452, etc.
What it does: Functions/macros usable in guard clauses are annotated with @doc guard: true metadata, separate from the doc string itself.
Why: This is machine-readable metadata. ExDoc and IEx can filter/display guard-eligible functions differently. It's orthogonal to the prose documentation.
Anti-pattern: Mentioning "allowed in guards" only in the doc string text, which tools can't parse programmatically.
Code example:
@doc """
Returns the absolute value of `number`.
...
## Examples
iex> abs(-1)
1
iex> abs(1)
1
"""
@doc guard: true
@spec abs(number) :: number
def abs(number) when is_number(number), do: ...
5. @doc false — Hiding from Documentation
Source: lib/elixir/lib/inspect.ex lines 410, 417; implicit via @impl true
What it does: @doc false explicitly hides a function from documentation generators. Also, using @impl true automatically sets @doc false (unless @doc is explicitly provided).
Why: Not all public functions are part of the user-facing API. Protocol implementations, internal helpers that must be public for technical reasons, and overridable defaults should be hidden from docs.
Anti-pattern: Leaving documentation on functions that are public only for technical reasons (e.g., protocol dispatch functions), confusing users about what to call.
Code example:
# Explicit hiding:
@doc false
def __protocol__(:module), do: __MODULE__
# Implicit via @impl:
@impl true # automatically sets @doc false
def init(counter) do
{:ok, counter}
end
6. @moduledoc false — Hiding Modules
Source: Common pattern in internal modules (not shown in top-level files but documented in Module)
What it does: @moduledoc false makes an entire module invisible to documentation tools.
Why: Internal implementation modules (e.g., MyApp.Repo.Migrations.Internal) exist for code organization but shouldn't appear in user-facing docs.
Anti-pattern: Leaving @moduledoc undeclared on internal modules — ExDoc will show them with an empty documentation page, confusing users.
Code example:
defmodule MyApp.Internal.Helper do
@moduledoc false
# This module exists but won't appear in docs
def helper_function(x), do: x * 2
end
7. Mermaid Diagrams in Documentation
Source: lib/elixir/lib/gen_server.ex lines 14–20
What it does: Embeds Mermaid diagram syntax directly in @moduledoc to illustrate architectural patterns (client-server message flow).
Why: Visual diagrams communicate architecture faster than prose. ExDoc renders Mermaid natively, so documentation can include live-rendered flowcharts.
Anti-pattern: Describing complex interaction patterns only in prose when a diagram would be clearer.
Code example:
@moduledoc """
A behaviour module for implementing the server of a client-server relation.
```mermaid
graph BT
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
A & B & C -->|request| GenServer
GenServer -.->|reply| A & B & C
Example
... """
---
## 8. Admonition Blocks in Documentation
**Source:** `lib/elixir/lib/gen_server.ex` lines 88–95, `lib/elixir/lib/supervisor.ex` lines 34–38
**What it does:** Uses markdown admonition syntax (`> #### Title {: .info}`) to highlight important callouts — especially for `use ModuleName` behavior documentation.
**Why:** Admonitions draw attention to critical information that might otherwise be buried in prose. The `.info`, `.warning`, `.neutral` classes provide visual differentiation.
**Anti-pattern:** Burying critical behavioral information (like what `use` does) in regular paragraphs that users skim over.
**Code example:**
```elixir
@moduledoc """
...
> #### `use GenServer` {: .info}
>
> When you `use GenServer`, the `GenServer` module will
> set `@behaviour GenServer` and define a `child_spec/1`
> function, so your module can be used as a child
> in a supervision tree.
...
"""
9. @doc deprecated: Soft Deprecation
Source: lib/elixir/lib/module.ex lines 163–180
What it does: Uses @doc deprecated: "Use X instead" for soft deprecation (documentation-only warning) vs. @deprecated "reason" for hard deprecation (compiler warning).
Why: Two levels of deprecation serve different needs. Soft deprecation signals "we recommend the alternative" without breaking builds. Hard deprecation actively warns at compile time.
Anti-pattern: Using @deprecated for things that aren't truly deprecated yet (just discouraged), creating noise in build outputs.
Code example:
# Soft deprecation — docs only, no compiler warning:
@doc deprecated: "Use Kernel.length/1 instead"
def size(keyword) do
length(keyword)
end
# Hard deprecation — compiler warning emitted:
@deprecated "Use Kernel.length/1 instead"
def size(keyword) do
length(keyword)
end
10. Callback Documentation Convention
Source: lib/elixir/lib/gen_server.ex lines 584–646 (handle_call docs)
What it does: Each @callback is preceded by a comprehensive @doc that explains: what triggers the callback, what the parameters mean, every possible return value and its effect, when the callback is optional, and cross-references to related callbacks.
Why: Callback documentation is the primary teaching tool for behaviour implementers. Since users must write these functions, the docs must be complete enough to implement from.
Anti-pattern: Documenting callbacks with only a one-liner, forcing users to read source code or external guides.
Code example:
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
reply is received (unless the call times out or nodes are disconnected).
`request` is the request message sent by a `call/3`, `from` is a 2-tuple
containing the caller's PID and a term that uniquely identifies the call, and
`state` is the current state of the `GenServer`.
Returning `{:reply, reply, new_state}` sends the response `reply` to the
caller and continues the loop with new state `new_state`.
Returning `{:noreply, new_state}` does not send a response to the caller and
continues the loop with new state `new_state`. The response must be sent with
`reply/2`.
...
This callback is optional. If one is not implemented, the server will fail
if a call is performed against it.
"""
@callback handle_call(request :: term, from, state :: term) ::
{:reply, reply, new_state}
| ...
when reply: term, new_state: term, reason: term
11. Documentation with Link References (c: and t: prefixes)
Source: lib/elixir/lib/gen_server.ex throughout the @moduledoc
What it does: Uses ExDoc link syntax like c:init/1 (callback reference), t:server/0 (type reference), and backtick function references like `start_link/2` to create navigable cross-references.
Why: Documentation is a hypertext. Cross-linking lets users navigate between related concepts. The c: and t: prefixes disambiguate between functions, callbacks, and types with the same name.
Anti-pattern: Mentioning related functions/types by name without linking them, requiring users to manually search.
Code example:
@moduledoc """
...
`c:init/1` transforms our initial argument to the initial state for the
GenServer. `c:handle_call/3` fires when the server receives a synchronous
`pop` message...
Each call to `GenServer.call/3` results in a message
that must be handled by the `c:handle_call/3` callback in the GenServer.
...
"""