Best Practices
49.1 Idiomatic Elixir
Return {:ok, value} / {:error, reason}; provide ! variants that raise.
Pattern match in function heads instead of nested conditionals.
Use with for happy-path chains.
Data first in function arguments (pipe-friendly).
Prefer small, pure functions; push side effects to the edges.
Use structs for domain entities; @enforce_keys for required fields.
Don't overuse processes — "use processes for runtime properties, not code organization".
Supervise every long-lived process.
Prefer explicit alias/import only: over use.
Document public functions with @doc and doctests; add typespecs.
Never convert user input to atoms.
Keep GenServer callbacks thin; put logic in pure modules ("functional core, imperative shell").
49.2 Anti-patterns (from the official docs)
Long parameter lists group into maps/structs/keyword opts Comments explaining obscure code clear names, small functions Complex else in with normalize errors in private functions Dynamic atom creation
String.to_existing_atom/1 or explicit mapping Non-assertive map access (map[:key] when key must exist) map.key or pattern matching
Boolean obsession (admin: true, editor: false) role::admin Exceptions for control flow structs / custom types Unrelated multi-clause functions Using app config in libraries pass options explicitly Code organization by process modules + functions Scattered process interfaces one module wraps all messages to a process start under a supervisor Sending unnecessary data to processes send only what's needed
Compile-time dependencies via macros everywhere runtime calls; reduce recompilation
49.3 Project structure
lib/my_app/ — domain (contexts).
lib/my_app_web/ — interfaces (web, API).
test/support/ — test helpers & factories.
Boundary enforcement: boundary library.