$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

AI Coding Rules for Elixir Teams

Elixir's actor model and functional patterns are unique — AI assistants trained on OOP languages need explicit rules to generate idiomatic Elixir.

8 min read·December 3, 2025

AI trained on OOP languages writes non-idiomatic Elixir by default

Pipe operators, pattern matching, GenServer design, and Phoenix conventions

Why Elixir Needs Specific AI Coding Rules

Elixir is built on the BEAM virtual machine — the same runtime that powers Erlang's legendary fault tolerance. Its programming model is fundamentally different from mainstream languages: immutable data, pattern matching, processes (not threads), supervision trees, and the pipe operator for data transformation. AI assistants trained mostly on Python, JavaScript, and Java struggle with all of these concepts.

The most common AI failures in Elixir: generating mutable-style code with variables that shadow previous bindings, ignoring the pipe operator in favor of nested function calls, creating modules that look like Java classes with state, misusing GenServers for everything instead of plain functions, and writing Phoenix controllers that bypass the plug pipeline.

Elixir has a small but passionate community with strong conventions. Your CLAUDE.md ensures the AI respects those conventions — writing Elixir that looks like it was written by an Elixir developer, not a Java developer learning Elixir.

Rule 1: Functional Patterns and Pipe Operator

The rule: 'Write functional Elixir. All data is immutable — never expect mutation. Use the pipe operator (|>) for data transformation chains. Prefer small, composable functions over large functions with multiple responsibilities. Use pattern matching for control flow, not if/else chains. Functions should take data as the first argument to enable piping.'

For the pipe operator: 'Transform data with pipes: data |> validate() |> normalize() |> persist(). Every public function should accept the primary data as its first argument. If a function needs configuration, make it the second argument: data |> process(opts). Never nest more than 2 function calls — use pipes instead.'

The pipe operator is Elixir's signature feature. AI code that doesn't use pipes is immediately recognizable as 'not Elixir.' This single rule transforms AI output from nested-call-style to idiomatic pipeline-style.

💡 Pipe Everything

The pipe operator (|>) is Elixir's signature. AI code without pipes is immediately non-idiomatic. Rule: 'Never nest more than 2 function calls — pipe instead.' Transforms AI output instantly.

Rule 2: Pattern Matching Everywhere

The rule: 'Use pattern matching for: function clause selection, destructuring, control flow, and error handling. Define multiple function clauses with different patterns instead of single functions with conditionals. Use with for chaining pattern-matched operations. Use case and cond instead of if/else for multi-branch logic.'

For function clauses: 'Define success and error handling as separate clauses: def process({:ok, data}), do: ... and def process({:error, reason}), do: ... The BEAM dispatches to the correct clause automatically — no need for conditional logic inside the function.'

For the with construct: 'Use with for operations that might fail at any step: with {:ok, user} <- fetch_user(id), {:ok, token} <- generate_token(user), do: {:ok, token}. The with expression short-circuits on the first non-matching pattern, making error handling clean and linear.'

  • Multiple function clauses over conditionals inside functions
  • Pattern match on function arguments — {:ok, data} and {:error, reason}
  • with for chaining fallible operations — short-circuits on first failure
  • case for multi-branch matching — cond for condition-based branching
  • Destructure in function heads: def handle({:user, name, age}), not def handle(tuple)

Rule 3: GenServers and OTP Design

AI assistants tend to reach for GenServer for everything — it's the most visible Elixir/OTP concept. But GenServers are for stateful processes, not general computation. Most business logic should be in plain modules with plain functions.

The rule: 'Use plain modules and functions for stateless business logic — no GenServer needed. Use GenServer only when you need: persistent state across calls, a named process for coordination, or rate limiting/batching. Use Agent for simple state (key-value stores). Use Task for one-off async operations. Use Supervisor for fault tolerance — every GenServer should be supervised.'

For GenServer design: 'Keep GenServer callbacks thin — delegate business logic to separate modules. Use handle_call for synchronous operations, handle_cast for fire-and-forget, handle_info for messages from other sources. Always handle unexpected messages in handle_info with a catch-all clause.'

⚠️ GenServer Overuse

AI reaches for GenServer for everything. Most business logic should be plain modules with plain functions. GenServer is only for persistent state, named processes, or rate limiting.

Rule 4: Phoenix Framework Conventions

The rule: 'Follow Phoenix conventions strictly. Contexts (lib/my_app/) contain business logic — controllers never access the database directly. Use changesets for all data validation. Use Phoenix.LiveView for interactive features — not JavaScript unless absolutely necessary. Use the Phoenix router for all routing — never bypass the plug pipeline.'

For LiveView: 'Use LiveView for all interactive server-rendered UI. Keep LiveView modules focused — one LiveView per page/feature. Use live components for reusable UI elements. Handle events with handle_event callbacks. Use assign and socket for state management — never store state outside the socket.'

For Ecto: 'Use Ecto.Repo for all database operations. Define schemas in context modules. Use changesets with cast, validate_required, and custom validations. Use Ecto.Multi for transactions that span multiple operations. Preload associations explicitly — never rely on lazy loading.'

  • Contexts for business logic — controllers delegate to contexts
  • Changesets for all validation — never validate in controllers
  • LiveView for interactive UI — JavaScript only when necessary
  • Ecto.Multi for multi-step transactions
  • Explicit preloads — no lazy loading (it doesn't exist in Ecto anyway)
ℹ️ Contexts Matter

Phoenix contexts are the boundary between your web layer and business logic. Controllers never touch the database — they call context functions. This separation is what makes Phoenix apps maintainable.

Rule 5: Testing with ExUnit

The rule: 'Use ExUnit for all tests. Use describe blocks to group related tests. Use setup callbacks for test fixtures. Use ExUnit.CaptureLog and CaptureIO for testing side effects. Use Mox for mocking — define behaviours (callbacks) for mockable modules. Use Ecto.Adapters.SQL.Sandbox for database test isolation.'

For property-based testing: 'Use StreamData for property-based testing of pure functions. Define custom generators for domain types. Property tests complement ExUnit tests — they don't replace them.'

For Phoenix testing: 'Use ConnTest for controller/plug testing. Use LiveViewTest for LiveView testing — simulate user events and assert on rendered HTML. Test contexts with DataCase — test through the context API, not through Ecto directly.'

Complete Elixir Rules Template

Consolidated template for Elixir/Phoenix teams.

  • Pipe operator for all data transformations — never nest more than 2 calls
  • Pattern matching for control flow — multiple function clauses over conditionals
  • with for chaining fallible operations — {:ok, _} and {:error, _} tuples
  • Plain modules for stateless logic — GenServer only for stateful processes
  • Phoenix contexts for business logic — controllers are thin delegation layers
  • Changesets for all validation — Ecto.Multi for transactions
  • LiveView for interactive UI — handle_event for user actions
  • ExUnit + Mox + StreamData — Sandbox for DB isolation, behaviours for mocking
AI Coding Rules for Elixir Teams — RuleSync Blog