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.