CODING_CONVENTIONS.md
Rama main · réplica de lectura del repositorio original.
# Elixir Coding Conventions
This document defines idiomatic Elixir patterns for any project running on the BEAM. These are not style preferences — they reflect how the runtime actually works. Writing Elixir that looks like Python or Ruby is not acceptable, even if it compiles and runs.
---
## The BEAM Philosophy
The BEAM is not a faster Python runtime. It is a fault-tolerant, concurrent, distributed virtual machine designed for systems that must run forever. Every pattern in this document exists because it maps to how the BEAM works:
- **Processes are cheap** — spawn them, let them crash, restart them
- **Immutability is enforced** — data does not change, it is transformed
- **Pattern matching is the primary dispatch mechanism** — not if/else chains
- **Supervision is built into the runtime** — use it, do not work around it
- **"Let it crash"** is a design principle, not laziness
If you are uncomfortable with any of these, read [Programming Elixir](https://pragprog.com/titles/elixir16/programming-elixir-1-6/) before contributing.
---
## Formatting
Run `mix format` before every commit. No exceptions. CI enforces this.
A `.formatter.exs` is in the repository root. Do not modify it without discussion.
---
## Pattern Matching
Pattern matching is the primary tool for control flow, destructuring, and dispatch. Use it everywhere.
### Match in function heads, not in the body
```elixir
# Wrong
def process(result) do
if elem(result, 0) == :ok do
value = elem(result, 1)
do_something(value)
end
end
# Right
def process({:ok, value}), do: do_something(value)
def process({:error, reason}), do: handle_error(reason)
```
### Match in receive and case, not with conditionals
```elixir
# Wrong
def handle_message(msg) do
if msg[:type] == "ping" do
:pong
else
:unknown
end
end
# Right
def handle_message(%{"type" => "ping"}), do: :pong
def handle_message(_msg), do: :unknown
```
### Use guards for type and value constraints
```elixir
def divide(a, b) when is_number(a) and is_number(b) and b != 0 do
{:ok, a / b}
end
def divide(_, 0), do: {:error, :division_by_zero}
def divide(_, _), do: {:error, :invalid_arguments}
```
---
## Error Handling
### Return tagged tuples — never raise for expected failures
```elixir
# Wrong — raises for expected conditions
def find_user!(id) do
Repo.get!(User, id)
end
# Right — returns a tagged tuple
def find_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
```
### Use `with` for multi-step operations
```elixir
# Wrong — nested case pyramid
case authenticate(token) do
{:ok, user} ->
case authorize(user, :write) do
:ok ->
case save(data) do
{:ok, record} -> {:ok, record}
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
# Right
with {:ok, user} <- authenticate(token),
:ok <- authorize(user, :write),
{:ok, record} <- save(data) do
{:ok, record}
end
```
### Use `Repo.get` not `Repo.get!` + rescue
```elixir
# Wrong — try/rescue for flow control is Python thinking
try do
record = Repo.get!(Model, id)
{:ok, record}
rescue
Ecto.NoResultsError -> {:error, :not_found}
end
# Right
case Repo.get(Model, id) do
nil -> {:error, :not_found}
record -> {:ok, record}
end
```
### Use `Integer.parse` not `String.to_integer` + rescue
```elixir
# Wrong
try do
{:ok, String.to_integer(value)}
rescue
ArgumentError -> {:error, :invalid}
end
# Right
case Integer.parse(value) do
{n, ""} -> {:ok, n}
_ -> {:error, :invalid}
end
```
### Never swallow errors silently
```elixir
# Wrong — hides bugs completely
try do
do_something()
rescue
_ -> nil
end
# Acceptable — at minimum log what happened
try do
do_something()
rescue
e ->
Logger.warning("do_something failed: #{Exception.message(e)}", error: e)
{:error, :unexpected}
end
# Best — let it crash, let the supervisor restart
do_something()
```
Silent `rescue _` makes the system appear healthy when it is not. Supervisors exist to handle crashes. Use them.
### `!` functions are for programmer errors only
Bang functions (`get!`, `fetch!`, `parse!`) raise on failure. Use them only when failure means the program itself is wrong — misconfigured, missing required data at boot time. Never use them for expected runtime failures like "user not found" or "invalid input."
---
## Atoms
### Never create atoms from runtime strings
Atoms are never garbage collected. Each unique atom created from a runtime string is a permanent memory allocation.
```elixir
# Wrong — memory leak, will eventually crash the VM
key = String.to_atom("provider_#{id}")
key = :"#{module_name}_handler"
# Right — use tuples as keys
key = {:provider, id}
key = {module_name, :handler}
```
The only acceptable use of `String.to_atom/1` is with a compile-time known, bounded set of values. If the value comes from user input, a database, or any external source, use a tuple or string key instead.
`String.to_existing_atom/1` is safer but still requires the atom to have been created at compile time.
---
## Pipes
### Only pipe when there is an actual transformation chain
```elixir
# Wrong — single-value pipe adds noise
result = value |> transform()
# Right
result = transform(value)
```
### Keep pipe chains readable — one transformation per line
```elixir
# Wrong — unreadable wall of pipes
result = input |> String.trim() |> String.downcase() |> String.replace(" ", "_") |> String.slice(0, 50)
# Right
result =
input
|> String.trim()
|> String.downcase()
|> String.replace(" ", "_")
|> String.slice(0, 50)
```
---
## Control Flow
### Prefer `unless` over `if not` for single-branch negation
```elixir
# Wrong
if not valid?(x), do: handle_invalid(x)
# Right
unless valid?(x), do: handle_invalid(x)
```
### Never use `unless...else`
Double negation is hard to reason about. `unless` with an `else` branch is explicitly discouraged by the Elixir style guide.
```elixir
# Wrong
unless disabled? do
:active
else
:inactive
end
# Right
if disabled? do
:inactive
else
:active
end
```
### Remove identity case expressions
```elixir
# Wrong — noise
result = case value do
x -> x
end
# Right
result = value
```
### Avoid deeply nested conditionals — flatten with `with` or function heads
```elixir
# Wrong
def handle(params) do
if Map.has_key?(params, :user_id) do
if Map.has_key?(params, :action) do
if params.action in [:read, :write] do
execute(params)
else
{:error, :invalid_action}
end
else
{:error, :missing_action}
end
else
{:error, :missing_user_id}
end
end
# Right
def handle(%{user_id: _, action: action} = params) when action in [:read, :write] do
execute(params)
end
def handle(%{user_id: _, action: _}), do: {:error, :invalid_action}
def handle(%{action: _}), do: {:error, :missing_user_id}
def handle(_), do: {:error, :missing_action}
```
---
## Lists and Enumerables
### Never append with `++` in loops
`list ++ [item]` copies the entire list on every iteration. This is O(n²).
```elixir
# Wrong — O(n²)
Enum.reduce(items, [], fn item, acc ->
acc ++ [transform(item)]
end)
# Right — prepend O(1), reverse once O(n)
items
|> Enum.reduce([], fn item, acc -> [transform(item) | acc] end)
|> Enum.reverse()
# Better — just use Enum.map
Enum.map(items, &transform/1)
```
### Prefer `Enum` over manual recursion for collection operations
```elixir
# Wrong — manual recursion for a standard operation
defp sum([]), do: 0
defp sum([h | t]), do: h + sum(t)
# Right
Enum.sum(items)
```
### Use `Stream` for lazy evaluation of large collections
```elixir
# Wrong — loads everything into memory
File.read!("large_file.log")
|> String.split("\n")
|> Enum.filter(&String.contains?(&1, "ERROR"))
|> Enum.take(100)
# Right — lazy, processes line by line
File.stream!("large_file.log")
|> Stream.filter(&String.contains?(&1, "ERROR"))
|> Enum.take(100)
```
---
## Strings
### Use string interpolation, not concatenation
```elixir
# Wrong
"Hello " <> name <> ", you are " <> Integer.to_string(age) <> " years old."
# Right
"Hello #{name}, you are #{age} years old."
```
### Use sigils for complex strings and patterns
```elixir
# Regex
pattern = ~r/^\d{4}-\d{2}-\d{2}$/
# Multiline strings
query = """
SELECT *
FROM users
WHERE active = true
"""
# String lists
roles = ~w(admin editor viewer)
```
---
## Structs and Maps
### Use structs for domain entities, maps for ad-hoc data
```elixir
# Wrong — map for a domain entity with known fields
user = %{name: "Alex", email: "alex@example.com", role: :admin}
# Right — struct enforces the shape
defmodule User do
defstruct [:name, :email, :role]
end
user = %User{name: "Alex", email: "alex@example.com", role: :admin}
```
### Use `@enforce_keys` for required fields
Fields that must always be present should be enforced at compile time, not validated at runtime.
```elixir
# Wrong — missing required fields only fail at runtime, maybe
defmodule Connection do
defstruct [:host, :port, timeout: 5_000]
end
# Right — missing :host or :port raises at compile time
defmodule Connection do
@enforce_keys [:host, :port]
defstruct [:host, :port, timeout: 5_000]
end
```
### Always match on the struct type when pattern matching structs
```elixir
# Wrong — matches any map with a name key
def greet(%{name: name}), do: "Hello #{name}"
# Right — only matches User structs
def greet(%User{name: name}), do: "Hello #{name}"
```
### Use `Map.get/3` with a default, not `Map.get/2` + nil check
```elixir
# Wrong
timeout = Map.get(config, :timeout)
timeout = if timeout == nil, do: 5000, else: timeout
# Right
timeout = Map.get(config, :timeout, 5000)
```
---
## OTP and Processes
### Always use Task.Supervisor for fire-and-forget tasks
```elixir
# Wrong — crash is invisible, no restart, no monitoring
Task.start(fn -> do_background_work() end)
# Right — crash is reported, supervised
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> do_background_work() end)
```
Add the supervisor to your application tree:
```elixir
# application.ex
children = [
{Task.Supervisor, name: MyApp.TaskSupervisor},
# ...
]
```
### Use `handle_continue/2` for post-init work — never `send(self(), :init)`
```elixir
# Wrong — works, but sends a message through the mailbox unnecessarily;
# another message could arrive and be processed before :do_init
def init(args) do
send(self(), :do_init)
{:ok, initial_state(args)}
end
def handle_info(:do_init, state) do
{:noreply, do_heavy_init(state)}
end
# Right — handle_continue runs before any other message is processed
def init(args) do
{:ok, initial_state(args), {:continue, :init}}
end
def handle_continue(:init, state) do
{:noreply, do_heavy_init(state)}
end
```
### Do not call GenServer functions from within the same GenServer
This causes a deadlock. Extract the logic into a private function and call it directly.
```elixir
# Wrong — deadlock
def handle_call(:do_thing, _from, state) do
result = GenServer.call(__MODULE__, :helper)
{:reply, result, state}
end
# Right
def handle_call(:do_thing, _from, state) do
result = compute_helper(state)
{:reply, result, state}
end
defp compute_helper(state), do: ...
```
### Keep GenServer state minimal and callbacks thin
```elixir
# Wrong — business logic buried in callback
def handle_call({:process, data}, _from, state) do
result =
data
|> validate()
|> enrich()
|> persist()
|> notify()
{:reply, result, %{state | last_processed: DateTime.utc_now()}}
end
# Right — callback delegates to pure functions
def handle_call({:process, data}, _from, state) do
result = process(data)
{:reply, result, update_state(state)}
end
defp process(data) do
data
|> validate()
|> enrich()
|> persist()
|> notify()
end
defp update_state(state), do: %{state | last_processed: DateTime.utc_now()}
```
### Use `GenServer.call` for synchronous operations, `GenServer.cast` for fire-and-forget
```elixir
# Synchronous — caller waits for result
GenServer.call(pid, {:get, key})
# Asynchronous — caller does not wait
GenServer.cast(pid, {:update, key, value})
```
### Do not store large data in GenServer state
GenServer state lives in the process heap. Large binaries, growing lists, or cached datasets should live in ETS, a database, or a dedicated cache process.
### Use `Process.send_after` for recurring work, not `receive` loops
```elixir
# In GenServer init
def init(state) do
schedule_tick()
{:ok, state}
end
def handle_info(:tick, state) do
do_periodic_work()
schedule_tick()
{:noreply, state}
end
defp schedule_tick do
Process.send_after(self(), :tick, :timer.seconds(30))
end
```
### Never use the process dictionary for application state
`Process.put/2` and `Process.get/2` are global mutable state scoped to a process. They are
invisible to callers, invisible to supervisors, and make code untestable. The only acceptable
uses are Logger metadata and library internals.
```elixir
# Wrong — hidden state, impossible to test or inspect
Process.put(:current_user, user)
user = Process.get(:current_user)
# Right — pass state explicitly
def handle_request(conn, user) do
do_work(conn, user)
end
```
### Supervision strategies
| Strategy | Use when |
|---|---|
| `:one_for_one` | Children are independent — default choice |
| `:one_for_all` | Children are interdependent — restart all on any failure |
| `:rest_for_one` | Children have ordered dependencies — restart failed + all started after it |
---
## Module Design
### `alias`, `use`, `import` — pick the right tool
```elixir
# alias — resolves a module name locally, no code is imported
alias MyApp.Accounts.User
# Now %User{} works instead of %MyApp.Accounts.User{}
# import — brings functions into scope; avoid unless the benefit is clear
import Ecto.Query, only: [from: 2, where: 3]
# use — injects code via __using__/1 macro, significant side effect
# Only use when a behaviour or macro injection is explicitly required
use GenServer
use Phoenix.LiveView
```
Prefer `alias` over `import`. Use `use` only when the library requires it (GenServer, LiveView,
Ecto.Schema, etc.). Never `use` a module just to avoid typing the full name.
### One module, one responsibility
A module that does too many things should be split. Signs a module needs splitting:
- More than ~300 lines
- Functions that could be grouped into clearly distinct namespaces
- Mix of pure business logic and side effects (DB, HTTP, IO)
### Use context modules (Phoenix pattern) for domain boundaries
```
MyApp.Accounts # User, Session, Token operations
MyApp.Accounts.User # Schema only
MyApp.Content # Post, Comment, Tag operations
MyApp.Content.Post # Schema only
```
Context modules are the public API. Schemas are private data structures.
### Extract shared helpers into dedicated modules
```elixir
# Wrong — same utility function copy-pasted across 5 modules
defp blank?(nil), do: true
defp blank?(""), do: true
defp blank?(_), do: false
# Right — one module, used everywhere
defmodule MyApp.Utils do
def blank?(nil), do: true
def blank?(""), do: true
def blank?(_), do: false
end
```
### Keep `defp` functions close to the `def` that calls them
Callers before helpers. Public functions at the top of the module.
---
## Typespecs and Documentation
### Every public function gets `@spec`
```elixir
@spec find_user(integer()) :: {:ok, User.t()} | {:error, :not_found}
def find_user(id) do
```
### Every module gets `@moduledoc`
```elixir
defmodule MyApp.Accounts do
@moduledoc """
Manages user accounts, sessions, and authentication.
"""
```
### Use `@doc false` for intentionally undocumented public functions
If a function must be public (e.g., for use in tests or callbacks) but should not appear in documentation:
```elixir
@doc false
def __callback__, do: :ok
```
### Consistent timestamp types
Choose one timestamp type and use it across all schemas. `:utc_datetime` is recommended. Do not mix `:naive_datetime` and `:utc_datetime`.
---
## Ecto
### Use changesets for all data mutations
Never write directly to the database without a changeset. Changesets provide validation, type casting, and an audit trail.
```elixir
# Wrong
Repo.insert!(%User{name: name, email: email})
# Right
%User{}
|> User.changeset(%{name: name, email: email})
|> Repo.insert()
```
### Use `Ecto.Multi` for multi-step database operations
`Repo.transaction/1` with `with` works for simple cases. `Ecto.Multi` is the right tool when
you have multiple named steps, need to inspect which step failed, or want composable transaction
building.
```elixir
# Acceptable for simple two-step cases
Repo.transaction(fn ->
with {:ok, user} <- create_user(params),
{:ok, _log} <- create_audit_log(user) do
user
else
{:error, reason} -> Repo.rollback(reason)
end
end)
# Right for complex multi-step operations — named steps, clear failure attribution
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.changeset(%User{}, params))
|> Ecto.Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(%Profile{}, %{user_id: user.id})
end)
|> Ecto.Multi.run(:notify, fn _repo, %{user: user} ->
Notifications.send_welcome(user)
end)
|> Repo.transaction()
|> case do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
{:error, :notify, reason, _} -> {:error, reason}
end
```
### Never trigger N+1 queries — preload associations explicitly
```elixir
# Wrong — each access hits the database inside the loop
orders = Repo.all(Order)
Enum.each(orders, fn order -> IO.inspect(order.items) end)
# Right — single query with preload
orders = Repo.all(Order) |> Repo.preload(:items)
Enum.each(orders, fn order -> IO.inspect(order.items) end)
```
If you access an association that has not been preloaded, Ecto raises `Ecto.Association.NotLoaded`.
Never rescue that — fix the preload.
### Use `from` queries instead of raw SQL for standard operations
```elixir
# Acceptable for simple queries
Repo.get(User, id)
Repo.all(User)
# Right for complex queries
from(u in User,
where: u.active == true and u.role == ^role,
order_by: [desc: u.inserted_at],
limit: ^limit
)
|> Repo.all()
```
---
## Security
### Never create atoms from user input
(See Atoms section above — this is also a security issue, not just a memory issue.)
### Always use `Plug.Crypto.secure_compare` for token/signature comparison
```elixir
# Wrong — timing attack vulnerability
signature == computed_signature
# Right — constant-time comparison
Plug.Crypto.secure_compare(signature, computed_signature)
```
### Never interpolate user input into HTML without escaping
```elixir
# Wrong — XSS vulnerability
html = "<p>Hello #{user_input}</p>"
# Right — in Phoenix templates, assign to a variable and let HEEx escape it
# In controllers, use Phoenix.HTML.html_escape/1
safe_input = Phoenix.HTML.html_escape(user_input)
```
### No hardcoded secrets, salts, or keys
```elixir
# Wrong
signing_salt: "my_app_salt"
secret_key: "hardcoded_secret"
# Right
signing_salt: Application.fetch_env!(:my_app, :signing_salt)
secret_key: System.fetch_env!("SECRET_KEY")
```
---
## Testing
### Test behaviour, not implementation
```elixir
# Wrong — tests internal function
test "parse_response transforms the map correctly" do
assert MyModule.parse_response(%{...}) == %{...}
end
# Right — tests public contract
test "fetch_user returns {:ok, user} when user exists" do
user = insert(:user)
assert {:ok, ^user} = Accounts.fetch_user(user.id)
end
```
### Default to `async: true`
Tests should run concurrently unless they touch shared mutable state: database without the Ecto
sandbox, named processes, ETS tables, or global config.
```elixir
# Default for unit tests and context tests using the Ecto sandbox
use MyApp.DataCase, async: true
# Only false when sharing state that cannot be isolated
use MyApp.DataCase, async: false
```
### Use ExUnit tags to organize test suites
```elixir
@tag :integration
test "creates a record in the database" do
@tag :unit
test "validates email format" do
```
Run subsets with `mix test --only unit` or `mix test --exclude integration`.
### Use ExMachina (or equivalent) for test data — never raw structs
```elixir
# Wrong — brittle, couples tests to schema details
user = %User{id: 1, name: "Test", email: "test@test.com", role: :admin, ...}
# Right — factory handles defaults, tests specify only what matters
user = insert(:user, role: :admin)
```
### Test the error paths, not just the happy path
Every function that returns `{:error, reason}` should have at least one test that exercises that path.
---
## What This Is Not
This document covers Elixir/OTP code. It does not define conventions for:
- Python code — follow PEP 8
- JavaScript/TypeScript — follow the project's ESLint config
- SQL — use Ecto query syntax where possible; raw SQL in separate files with comments
- Shell scripts — follow Google Shell Style Guide
---
## Resources
- [Elixir Style Guide](https://github.com/christopheradams/elixir_style_guide)
- [Credo](https://github.com/rrrene/credo) — static analysis for Elixir style
- [Dialyxir](https://github.com/jeremyjh/dialyxir) — typespec verification
- [Programming Elixir](https://pragprog.com/titles/elixir16/programming-elixir-1-6/) — Dave Thomas
- [Designing Elixir Systems with OTP](https://pragprog.com/titles/jgotp/designing-elixir-systems-with-otp/) — James Edward Gray II