git:20260510.fe97ca3 to git:20260905.88628fb

55 added, 51 removed. Audit A to A.

---
name: programming-elixir
title: "Elixir Development"
description: "OTP-first architecture, pattern matching, supervision design, and Phoenix conventions. Auto-activates in Elixir projects."
license: Apache-2.0
compatibility: "Requires Elixir and Erlang/OTP."
domains: developer
rules:
- file(mix.exs)
- content(elixir)
---
- ## Mental model
+ ## Overview
- Elixir is a functional language built on the BEAM, a runtime designed for concurrency, isolation, and fault tolerance. The big architectural lever is OTP — supervision trees and lightweight processes — not language features. "Let it crash" is not a slogan; it's the design: code the happy path, let supervisors restart on failure, and avoid defensive programming. Most maintenance pain comes from treating processes like threads, from rescuing too eagerly, and from spreading business logic across GenServers when plain modules would do.
+ Write Elixir as clear data transformations with explicit process and failure ownership. Research baseline: 2026-09-05, Elixir 1.20 stable, with gradual compiler type inference across language constructs. Inspect the project's Elixir/Erlang requirements and locked Phoenix/Ecto versions first. Elixir 1.20 requires OTP 27+; select a documented compatible pair rather than independently upgrading runtimes.
- ## Functional core
+ ## Mental model
- - Pure functions in modules form the bulk of a codebase; processes are for state, isolation, and concurrency — not for organization
- - Multi-clause functions with pattern matching replace `if`/`else`/`case` chains
- - Pipe (`|>`) when data flows linearly; `then/2` when the value isn't the first argument
- - `with` for happy-path chains of `{:ok, _}` / `{:error, _}` — the canonical control-flow construct for fallible pipelines
- - Tagged tuples for return values: `{:ok, value}` / `{:error, reason}` — never raise for expected outcomes
- - Structs (`defstruct`) for typed data; `@type` and `@spec` on the public API
+ Modules organize behavior; processes own concurrent activity and state. Supervision restores failed processes, not lost database transactions or external side effects. Validate untrusted input and represent expected failures explicitly; let unexpected defects fail where a deliberate supervision boundary can handle them.
- ## OTP architecture
+ ## Functions, data, and current typing
- - A supervision tree is the application skeleton — children listed in the `Application` module's `start/2`
- - Each process has one job; if a process needs to do two things, it's two processes
- - `GenServer` for stateful services with a clear public API (`MyServer.call(...)` wrapping `GenServer.call`)
- - `DynamicSupervisor` for children started at runtime; `PartitionSupervisor` for sharded workloads
- - `Task` and `Task.Supervisor` for one-shot async work
- - Choose restart strategies deliberately: `:one_for_one` for independent children, `:rest_for_one` when later children depend on earlier ones
- - Let processes crash on unexpected input — the supervisor restarts cleaner state than rescue blocks paper over
+ - Use functions and pattern matching for ordinary domain logic. A GenServer is not required to encapsulate a module or make it "OTP-first."
+ - Use clauses and guards when they clarify accepted shapes. Validate untrusted input before entering functions whose patterns assume internal invariants; don't add catch-all success defaults to hide malformed data.
+ - Keep `{:ok, value}`/`{:error, reason}` contracts consistent. Use `with` for dependent fallible steps, but normalize ambiguous failure shapes in their owning functions. A large `else` reconstructing which step failed signals unclear contracts.
+ - Use `case` for branching and pipes for linear transformations; do not force every condition into `with`. Keep error reasons useful to callers without leaking sensitive input.
+ - Elixir 1.20 infers types through expressions, guards, clauses, and dependency information. Treat verified-bug/dead-code warnings as evidence to investigate. This is not complete compile-time proof of program correctness.
+ - Keep `@spec` and `@type` for documented contracts and existing analysis tools. Do not invent new set-theoretic annotation syntax: user-supplied signatures and typed structs for that system remain future work in the 1.20 release.
+ - Structs provide a known shape, not automatic field validation. Use changesets or explicit constructors for boundary checks; `@enforce_keys` does not validate values.
+ - Keep external keys as strings or map them through an explicit allowlist. Unbounded `String.to_atom/1` can exhaust the atom table; `to_existing_atom/1` still raises and does not prove an atom is allowed for this operation.
+ - Use `Stream` for deferred traversal when needed; consume it deliberately. Avoid assuming laziness removes the memory cost of a later full materialization.
- ## Concurrency patterns
+ ## Process and task ownership
- - Processes are cheap (millions on a node) — spawn freely when isolation helps
- - Send messages, don't share state; the BEAM gives you message-passing for free
- - Long-running work in a `Task` under a supervisor, not in a controller or LiveView callback
- - Backpressure via GenStage / Flow / Broadway when producers can outpace consumers
- - Time-outs on every `GenServer.call`; the default 5 seconds is rarely what you want for slow operations
+ - Introduce a GenServer when serial access to state or a managed lifecycle is needed. Long callbacks block its mailbox; move independent work to supervised tasks with explicit result handling.
+ - Choose supervision strategy from dependency relationships: `:one_for_one` for independent children, `:rest_for_one` when later children depend on earlier ones. Specify restart/shutdown policy; a Task normally has temporary restart behavior.
+ - `Task.async` links caller and task, so task failure can terminate its caller. Use supervised `async_nolink` when that coupling is undesirable, and consume results, failures, and monitor messages.
+ - Bound fan-out with `Task.async_stream` or equivalent limits. Set concurrency according to downstream capacity, with a deliberate timeout policy. Lazy input alone does not make unbounded spawning safe.
+ - Task closures copy captured data into another process. Extract only required values; avoid capturing an entire LiveView socket or large state object.
+ - A `GenServer.call` timeout does not cancel work already accepted by the server. Account for uncertain completion before retrying a mutation; a cast provides no processing acknowledgement.
+ - Do not synchronously call a GenServer from itself; direct self-calls fail rather than becoming useful serialization. Keep internal computation in functions, or redesign the interaction.
+ - Supervision is not durable job storage. Work that must survive node loss needs the application's durable job mechanism and idempotent effects.
- ## Error handling
+ ## Persistence and Phoenix boundaries
- - `with` for chained fallible operations — one path for success, one clause per failure shape
- - Tagged tuples for expected failures; `raise` only for genuinely exceptional conditions
- - Provide context in errors: `{:error, {:not_found, %{type: :user, id: id}}}` beats `{:error, :not_found}`
- - Rescue at system boundaries (HTTP handlers, message consumers) where you must respond to the outside world
+ - Changesets cast allowed external fields, validate domain rules, and translate declared database constraint failures. Back race-sensitive invariants with actual database constraints; preflight checks alone are insufficient.
+ - Use `Ecto.Multi` for named dependent database operations when it improves clarity. Handle the failed operation and reason; returned `changes_so_far` contains prior operation results, while the transaction's database writes roll back. Transactions do not make external API calls reversible.
+ - Respect existing Phoenix context APIs so controllers and LiveViews share authorization and domain rules. Avoid blanket rules forbidding meaningful domain validation in changesets.
+ - Authorize protected operations on the server, including LiveView events. A hidden button and a successful initial mount do not establish ongoing authorization.
+ - In supported LiveView versions, use `assign_async`/`start_async` for lifecycle-managed work, with loading/error rendering. Capture needed values before starting work; avoid blocking callbacks with `Task.await`.
+ - Use streams for large changing collections when their identity/update model fits. They reduce retained collection state; they do not remove the need for query limits or pagination.
- ## Ecto and persistence
+ ## Example
- - Schemas describe database shape; changesets validate and cast at the boundary
- - Keep query composition explicit: small named functions returning queryables, composed at the call site
- - Multi-step writes use `Ecto.Multi` so the whole transaction rolls back on failure
- - Avoid putting business logic in changesets — they validate data, they don't make decisions
- - Migrations are forward-only in production; design rollback as a new migration
+ Accept only known external values without creating atoms or disguising errors:
- ## Phoenix and LiveView
+ ```elixir
+ defmodule Visibility do
+ @spec parse(term()) :: {:ok, :public | :private} | {:error, :invalid_visibility}
+ def parse("public"), do: {:ok, :public}
+ def parse("private"), do: {:ok, :private}
+ def parse(_), do: {:error, :invalid_visibility}
+ end
+ ```
- - Contexts (`Accounts`, `Billing`, `Inventory`) are the public API for a domain — controllers and LiveViews call contexts, not Ecto directly
- - LiveView holds UI state in `socket.assigns`; long work goes to a `Task` and streams results back via `handle_info`
- - Streams (`stream/3`) for large lists — they avoid sending the full collection on every diff
- - Channels for low-level WebSocket needs that LiveView doesn't fit
- - Function components and slots for composable UI; keep template logic minimal
+ The final clause returns an explicit expected failure. Internal callers can pattern-match the result without rescuing exceptions or silently selecting a default.
- ## Testing
+ ## Checklist
- - ExUnit with `async: true` for tests that don't touch shared state — the BEAM's isolation makes most tests parallelizable
- - Pattern-match in `assert`: `assert {:ok, %User{name: "Ada"}} = create_user(params)`
- - `describe` blocks group tests around a function; `setup` and `setup_all` for fixtures
- - Mox for behaviour-based mocks; define a behaviour, swap implementations in tests
- - Test contexts and pure modules directly; LiveView/controller tests are integration tests
+ - Verify Elixir/OTP compatibility and supported library APIs.
+ - Keep process boundaries motivated by concurrency/lifecycle, not code organization.
+ - Check expected failures, compiler warnings, atom handling, and boundary validation.
+ - Check task links, bounded work, timeouts, duplicate effects, and transaction limits.
+ - Use installed formatter, compilation checks, ExUnit, and existing analysis tools when authorized. Format checks alone do not prove compilation or behavior; use async tests only when their shared resources are isolated.
- ## Common pitfalls
+ ## References
- - `%{map | key: val}` only updates existing keys — `Map.put/3` for new
- - `String.to_atom/1` on user input leaks memory (atoms aren't garbage-collected) — use `String.to_existing_atom/1`
- - `Enum` materializes; `Stream` is lazy — use `Stream` for large or infinite sequences
- - A `GenServer` that calls itself synchronously deadlocks — use `cast`, or restructure
+ - [Elixir stable documentation](https://elixir-lang.org/docs/) and [1.20 typing release](https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/)
+ - [Elixir 1.20 OTP requirements](https://elixir.hexdocs.pm/1.20.0/changelog.html)
+ - [Official code anti-patterns](https://elixir.hexdocs.pm/code-anti-patterns.html)
+ - [Task ownership and concurrency](https://elixir.hexdocs.pm/Task.html)
+ - [GenServer call semantics](https://elixir.hexdocs.pm/GenServer.html) and [supervision policies](https://elixir.hexdocs.pm/Supervisor.html)
+ - [Ecto changesets](https://ecto.hexdocs.pm/Ecto.Changeset.html) and [transactions with Multi](https://ecto.hexdocs.pm/Ecto.Multi.html)
+ - [LiveView async operations and streams](https://phoenix-live-view.hexdocs.pm/Phoenix.LiveView.html)