Stack conventions
Elixir (and Phoenix)
Elixir applications and libraries on OTP, including Phoenix and Ecto, tested with ExUnit.
Elixir applications and libraries on OTP, including Phoenix and Ecto, tested with ExUnit.
Used for: elixir.
Principles
- Let it crash - write the happy path, supervise processes, and handle only the errors you can meaningfully recover from.
- Use pattern matching and multi-clause functions with guards instead of nested conditionals.
- Return tagged tuples ({:ok, value} / {:error, reason}) for expected failures and compose them with with; raise (bang functions) only for unexpected ones.
- Keep data immutable and functions pure; isolate side effects at the edges.
- Use processes for runtime concerns (state, concurrency, fault isolation), not for code organisation; prefer plain modules and functions.
- Start long-lived processes under a supervision tree with explicit restart strategies; never spawn unlinked processes that nobody supervises.
- In Phoenix, group domain logic in contexts with a public API; controllers, LiveViews and channels call contexts, never Repo directly.
- Validate external data with Ecto changesets (also for non-database data via embedded or schemaless changesets).
- Keep code formatted with mix format and clean under Credo; add typespecs to public functions.
Conventions
Accessibility
- In Phoenix templates and LiveView, use semantic HTML and core components with labels (<.input field={@form[:email]} label="Email" />).
- Manage focus after LiveView patches and announce flash messages (role="alert").
Concurrency
- A single GenServer is a serialisation point; avoid bottlenecks by partitioning (PartitionSupervisor) or using ETS.
- Always set timeouts on GenServer.call and Task.await consciously; handle {:EXIT} and :DOWN messages when monitoring.
- Use Oban (or the queue the app uses) for durable, retryable background jobs; make jobs idempotent with unique options.
Dependencies
- Specify Hex dependencies with ~> constraints; commit mix.lock.
- Prefer well-maintained packages from the Elixir ecosystem; check with mix hex.outdated.
Documentation
- @moduledoc and @doc on public modules and functions (@moduledoc false for internal ones); include examples as doctests.
- @spec on public functions and custom @type definitions; run Dialyzer (dialyxir) where the project uses it.
- Generate docs with ExDoc for libraries.
Errors
- Return {:ok, value} | {:error, reason} for expected outcomes; provide bang variants that raise for callers who prefer it.
- Do not use try/rescue for control flow; rescue only around third-party code that raises for expected cases.
- Let processes crash on unexpected errors and rely on supervisors to restart them into a known state.
- Use exception structs (defexception) with clear messages for errors you raise.
Logging
- Use Logger with metadata (Logger.metadata(request_id: id)) and appropriate levels; no IO.inspect left in code.
- Never log secrets or personal data; use Phoenix filter_parameters for request params.
- Emit :telemetry events for metrics rather than logging counts.
Naming
- snake_case for functions, variables and files; CamelCase for modules (MyApp.Accounts.User in lib/my_app/accounts/user.ex).
- Functions ending in ! raise on failure; predicate functions end in ? (guards that are macros start with is_).
- size for constant-time and length for linear-time operations, matching the standard library.
Performance
- Preload associations explicitly (Repo.preload or preload in queries) to avoid N+1 queries; Ecto never lazy loads.
- Use ETS or persistent_term for read-heavy shared data instead of funnelling reads through one GenServer.
- Stream large datasets (Repo.stream within a transaction, Stream module) rather than loading them fully.
- Avoid copying large binaries between processes; keep message payloads small.
Security
- Build queries with Ecto query syntax and pinned values (^value); never interpolate into fragment strings.
- Cast and validate params through changesets with explicit allowed fields (cast/3 field list).
- Keep Phoenix CSRF protection (protect_from_forgery) and secure headers (put_secure_browser_headers) in the browser pipeline.
- Never convert user input to atoms (String.to_atom); use String.to_existing_atom or explicit mappings, since atoms are not garbage collected.
- Run Sobelow for Phoenix security checks and mix deps.audit / mix hex.audit for dependencies.
- Keep secrets in runtime configuration (config/runtime.exs reading environment variables), never in compiled config or the repository.
- Never pass external input to System.cmd through a shell, Code.eval_string or :os.cmd (injection).
Structure
- One module per file; directory path mirrors the module name.
- In Phoenix, keep the MyApp (domain) and MyAppWeb (web) boundary; web modules depend on contexts, never the reverse.
- Configuration in config/*.exs; runtime secrets and environment-specific values in config/runtime.exs read from environment variables.
- Avoid compile-time application config (Application.compile_env only when truly needed); prefer runtime config.
Testing
- ExUnit with async: true wherever tests do not share global state; Ecto SQL Sandbox for database tests.
- Use describe blocks per function and setup callbacks for shared context.
- Mock only behaviours with Mox; avoid mocking modules you do not own directly.
- Test LiveViews with Phoenix.LiveViewTest and controllers with ConnTest; use doctests for pure functions.
- Use property-based tests (StreamData) for data transformations where valuable.
- Unit tests cover pure functions and contexts with Mox for external behaviours; integration tests use ConnTest, LiveViewTest and the Ecto sandbox against a real database.
- Name tests after the behaviour (
test "rejects an order without lines") inside a describe per function. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Is the code mix format clean and free of new Credo issues?
- Do functions return tagged tuples for expected failures and raise only in bang variants?
- Is pattern matching used instead of nested conditionals, with clauses ordered from specific to general?
- Are new long-lived processes supervised with an appropriate restart strategy?
- Are processes used only for state, concurrency or fault isolation?
- In Phoenix, do web modules call context functions rather than Repo or another context's internals?
- Are external params validated through changesets with explicit allowed fields?
- Are Ecto queries using pinned parameters, with associations preloaded to avoid N+1?
- Is String.to_atom avoided on user input?
- Do public functions have @doc and @spec?
- Are tests async where possible, using the Ecto sandbox and Mox against behaviours?
- Are background jobs durable and idempotent?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
mix-format |
format | mix format | no |
mix-compile-warnings |
static | mix compile | no |
credo |
lint | credo | no |
dialyzer |
typecheck | dialyxir | no |
sobelow |
security | sobelow | no |
deps-audit |
security | mix_audit | no |
gitleaks |
security | gitleaks | no |