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

11 KiB
Raw Blame History

Documentation Patterns

Patterns extracted from the Elixir standard library source code.


1. @moduledoc with Structured Sections

Source: lib/elixir/lib/gen_server.ex lines 6100+, lib/logger/lib/logger.ex lines 6200+

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 315335 (abs/1), lib/logger/lib/logger.ex lines 536540

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 1420

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 8895, `lib/elixir/lib/supervisor.ex` lines 3438

**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 163180

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 584646 (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

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