Files
Rodin 74101b513c chore: merge elixir-conventions and oban-conventions into sources/
Absorbed content from rodin/elixir-conventions and rodin/oban-conventions
into a sources/ directory. These are reference material — descriptive,
not prescriptive. Patterns that prove broadly applicable get promoted
into patterns/.

Part of taxonomy cleanup (issue #4):
- Pattern = prescriptive, follow these
- Convention/Source = reference, study for ideas

The original repos can now be archived.
2026-05-07 18:01:42 -07:00

7.7 KiB

Elixir Language Source: Convention Reference

Quick-reference for conventions extracted from the elixir-lang/elixir source code. Each entry: pattern name, location, example, when to use, when NOT to use, origin.


Version-Gated TODOs

Location: Throughout lib/

# TODO: Remove me on v2.0
# TODO: Deprecate me on Elixir v1.23
# TODO: Make an error on Elixir v2.0

When to use: Any backward-compatible code that should be removed at a known future version. Deprecation paths, compatibility shims, feature flags.

When NOT to use: Performance improvements ("make this faster someday"), refactoring desires, or anything without a clear version boundary. Those belong in issues, not TODOs.

Origin: Consistent throughout history. The pattern predates the repo's earliest commits — José's convention from the start.


Independent OTP Applications

Location: lib/ (6 top-level dirs)

lib/elixir/mix.exs    # The language core
lib/ex_unit/mix.exs   # Testing framework
lib/mix/mix.exs       # Build tool
lib/logger/mix.exs    # Logging
lib/iex/mix.exs       # Interactive shell
lib/eex/mix.exs       # Templates

When to use: When components have distinct lifecycle, deployment, or dependency requirements. When you want components to be independently testable.

When NOT to use: Small projects where the overhead of multiple applications exceeds the organizational benefit. If components always deploy together and never independently, a single app is simpler.

Origin: Elixir 0.x — the language was always structured this way.


Erlang for Performance-Critical Paths

Location: lib/elixir/src/ (33 .erl files)

%% elixir_tokenizer.erl — binary pattern matching is faster in Erlang
%% elixir_erl_pass.erl — code generation benefits from proximity to BEAM

When to use: When Erlang's binary pattern matching or NIF interface provides measurable performance advantage. When code must exist before Elixir's compiler is available (bootstrap).

When NOT to use: For new features that could be written in Elixir. The trajectory is always toward less Erlang. Don't write new Erlang unless profiling proves it's necessary.

Origin: The entire language started as Erlang (2011). Bootstrap file (elixir_bootstrap.erl) formalized as distinct from "Elixir written in Erlang" in 2013 (commit 260be7c8e).


Protocol Consolidation

Location: lib/elixir/lib/protocol.ex

# In mix.exs (default for prod):
consolidate_protocols: true

# Disable for tests:
consolidate_protocols: Mix.env() != :test

When to use: Always in production (it's the default). Consolidated protocols dispatch in two function calls instead of dynamic lookup.

When NOT to use: In tests where you define new protocol implementations at runtime. In dev if you're frequently recompiling protocol implementations.

Origin: PR adding consolidation to escript.build (2014, issue #2699). Later made default for Mix projects.


Code.Formatter via Inspect.Algebra

Location: lib/elixir/lib/code/formatter.ex (2,605 lines)

# The formatter is a library function:
Code.format_string!("def foo(  x,y),do: x+y")
# => "def foo(x, y), do: x + y"

# Internally uses Wadler-Lindig algebra:
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]

When to use: When building code generation tools, macros that produce source, or custom formatting rules. The algebra is available to any Elixir code.

When NOT to use: Don't reimplement formatting logic. Use Code.format_string!/2 or the mix format task. The algebra is for building formatters, not for end-user formatting.

Origin: Oct 7, 2017 (PR #6639, José Valim). Merged in 1 hour, zero review comments.


Mix Task Convention

Location: lib/mix/lib/mix/tasks/ (55 files)

defmodule Mix.Tasks.Deps.Clean do
  @moduledoc "Removes the given dependencies' build artifacts."
  @shortdoc "Deletes generated files and artifacts for dependencies"

  use Mix.Task
  @recursive true

  @impl true
  def run(args) do
    # ...
  end
end

When to use: Any command-line operation that should be available via mix <task>. One file per task, module name determines task name.

When NOT to use: Tasks that require interactive user input (use escript or IEx helpers instead). Tasks that need to run without Mix loaded.

Origin: Part of Mix since its creation. The one-file convention is strict — all 55 stdlib tasks follow it.


ExUnit.CaseTemplate

Location: lib/ex_unit/lib/ex_unit/case_template.ex (162 lines)

defmodule MyApp.DataCase do
  use ExUnit.CaseTemplate

  setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
    unless tags[:async], do: Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
    :ok
  end
end

# Usage:
defmodule MyApp.SomeTest do
  use MyApp.DataCase, async: true
end

When to use: When multiple test modules share setup logic, assertions, or helper functions. The inheritance model allows composition of test concerns.

When NOT to use: For test helpers that don't need lifecycle callbacks. Simple import or alias is cleaner for utility functions that don't need setup/setup_all.

Origin: Commit 1f491dfbe — "Add support to case templates." The pattern Phoenix adopted for ConnCase/DataCase comes directly from ExUnit.


Logger Wrapping OTP

Location: lib/logger/

# Elixir's Logger dispatches to Erlang's :logger
# Translation layer converts Erlang messages → Elixir format
# Filters and handlers use OTP's infrastructure

When to use: When OTP provides the infrastructure you need. Wrap it — don't replace it. Provide Elixir-idiomatic API on top.

When NOT to use: When OTP's solution has fundamental architectural limitations you can't work around with a wrapper (rare).

Origin: PR #9333 (Sep-Nov 2019). Rewrote Logger from custom dispatch to wrapping Erlang's :logger. ~2 month implementation.


BDFL Merge Pattern

Location: Throughout git history

PR #6639 (Formatter):  0 comments, merged in 1 hour, 2,605 new lines
PR #14021 (JSON):      38 review comments, 13 days, community input
PR #13385 (Duration):  75+116 comments, 33 days, community redirected

When to use: Foundational infrastructure where one person holds the architectural vision. The BDFL can ship without review when the change is self-evidently correct (formatter) or redirect community work when the abstraction isn't right (Duration).

When NOT to use: In team-maintained projects without a clear authority. In projects where consensus is required for API decisions. The BDFL model fails when the BDFL is wrong and no one can override.

Origin: José Valim has been sole authority since Elixir's creation (2011). The core team exists but José has final say on language design.


Type System Architecture (Set-Theoretic)

Location: lib/elixir/lib/module/types/ (13,034 lines, 7 files)

# Types are sets. Operations are set operations.
# descr.ex (6,301 lines) defines the algebra:
# - Union of types
# - Intersection of types
# - Difference (negation types)
# - Subtype checking via set inclusion

When to use: Understanding how Elixir's gradual type system works internally. The set-theoretic approach means types compose naturally — integer() | String.t() is literally a set union.

When NOT to use: This is internal compiler infrastructure. Don't depend on Module.Types internals — they change frequently (504 commits and counting).

Origin: Aug 2019 (PR #9270 by Eric Meadows-Jönsson). Originally just function clause exhaustiveness checking, now growing into full gradual typing. 96% of subsequent work by José.