# Elixir Expert

> Elixir and Phoenix expert including OTP, Ecto, and functional programming

- Skill: `oimiragieo/elixir-expert` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add oimiragieo/elixir-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/elixir-expert/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/oimiragieo/elixir-expert

---


# Elixir Expert

<identity>
You are a elixir expert with deep knowledge of elixir and phoenix expert including otp, ecto, and functional programming.
You help developers write better code by applying established guidelines and best practices.
</identity>

<capabilities>
- Review code for best practice compliance
- Suggest improvements based on domain patterns
- Explain why certain approaches are preferred
- Help refactor code to meet standards
- Provide architecture guidance
</capabilities>

<instructions>
### elixir expert

### elixir general engineering rule

When reviewing or writing code, apply these guidelines:

- Act as an expert senior Elixir engineer.
- When writing code, use Elixir, Phoenix, Docker, PostgreSQL, Tailwind CSS, LeftHook, Sobelow, Credo, Ecto, ExUnit, Plug, Phoenix LiveView, Phoenix LiveDashboard, Gettext, Jason, Swoosh, Finch, DNS Cluster, File System Watcher, Release Please and ExCoveralls.

</instructions>

<examples>
Example usage:
```
User: "Review this code for elixir best practices"
Agent: [Analyzes code against consolidated guidelines and provides specific feedback]
```
</examples>

## Elixir Language Patterns

### Pattern Matching

Pattern matching is fundamental to Elixir. Use it for:

**Function clauses:**

```elixir
def greet(%User{name: name, role: :admin}), do: "Hello Admin #{name}"
def greet(%User{name: name}), do: "Hello #{name}"
def greet(_), do: "Hello stranger"
```

**Case statements:**

```elixir
case {status, data} do
  {:ok, %{id: id} } when id > 0 -> process(id)
  {:error, reason} -> handle_error(reason)
  _ -> :unknown
end
```

**With statements for chaining:**

```elixir
with {:ok, user} <- fetch_user(id),
     {:ok, profile} <- fetch_profile(user),
     {:ok, settings} <- fetch_settings(profile) do
  {:ok, %{user: user, profile: profile, settings: settings} }
end
```

### Guards

Use guards to add constraints to pattern matching:

```elixir
def categorize(n) when is_integer(n) and n > 0, do: :positive
def categorize(n) when is_integer(n) and n < 0, do: :negative
def categorize(n) when is_integer(n), do: :zero

def process_map(map) when map_size(map) == 0, do: :empty
def process_map(map) when is_map(map), do: :has_data
```

### Pipe Operator

The pipe operator `|>` improves readability:

```elixir
# Instead of nested calls
result = String.trim(String.downcase(String.reverse(input)))

# Use pipes
result =
  input
  |> String.reverse()
  |> String.downcase()
  |> String.trim()
```

Pipe into case for handling results:

```elixir
user_id
|> fetch_user()
|> case do
  {:ok, user} -> process_user(user)
  {:error, :not_found} -> create_user()
  {:error, reason} -> {:error, reason}
end
```

## OTP (Open Telecom Platform) Patterns

### GenServer

GenServer is the foundation for stateful processes:

```elixir
defmodule Counter do
  use GenServer

  # Client API
  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  def increment do
    GenServer.cast(__MODULE__, :increment)
  end

  def get do
    GenServer.call(__MODULE__, :get)
  end

  # Server Callbacks
  @impl true
  def init(initial_value) do
    {:ok, initial_value}
  end

  @impl true
  def handle_cast(:increment, state) do
    {:noreply, state + 1}
  end

  @impl true
  def handle_call(:get, _from, state) do
    {:reply, state, state}
  end
end
```

### Supervisor

Supervisors manage process lifecycles:

```elixir
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Database
      MyApp.Repo,

      # PubSub
      {Phoenix.PubSub, name: MyApp.PubSub},

      # GenServers
      {MyApp.Cache, []},
      {MyApp.Worker, []},

      # Endpoint (starts web server)
      MyAppWeb.Endpoint
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

**Supervisor strategies:**

- `:one_for_one` - restart only failed child
- `:one_for_all` - restart all children if one fails
- `:rest_for_one` - restart failed child and those started after it

### Agent

For simple state management:

```elixir
{:ok, agent} = Agent.start_link(fn -> %{} end)
Agent.update(agent, fn state -> Map.put(state, :key, "value") end)
Agent.get(agent, fn state -> Map.get(state, :key) end)
```

## Phoenix Framework

### Controllers

```elixir
defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, :index, users: users)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully")
        |> redirect(to: ~p"/users/#{user}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end
end
```

### Phoenix LiveView

For real-time interactive UIs:

```elixir
defmodule MyAppWeb.CounterLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  @impl true
  def handle_event("increment", _params, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <h1>Count: <%= @count %></h1>
      <button phx-click="increment">+</button>
    </div>
    """
  end
end
```

**PubSub for broadcasting:**

```elixir
# Subscribe
Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")

# Broadcast
Phoenix.PubSub.broadcast(MyApp.PubSub, "updates", {:new_data, data})

# Handle in LiveView
@impl true
def handle_info({:new_data, data}, socket) do
  {:noreply, assign(socket, :data, data)}
end
```

### Channels

For WebSocket communication:

```elixir
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  @impl true
  def join("room:" <> room_id, _payload, socket) do
    {:ok, assign(socket, :room_id, room_id)}
  end

  @impl true
  def handle_in("new_message", %{"body" => body}, socket) do
    broadcast!(socket, "new_message", %{body: body})
    {:reply, :ok, socket}
  end
end
```

## Ecto Database Patterns

### Schemas and Changesets

```elixir
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer

    has_many :posts, MyApp.Content.Post
    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
    |> unique_constraint(:email)
  end
end
```

### Queries

```elixir
import Ecto.Query

# Basic queries
query = from u in User, where: u.age > 18, select: u

# Composable queries
def for_age(query, age) do
  from u in query, where: u.age > ^age
end

def ordered(query) do
  from u in query, order_by: [desc: u.inserted_at]
end

# Chain them
User
|> for_age(18)
|> ordered()
|> Repo.all()

# Joins and preloads
from u in User,
  join: p in assoc(u, :posts),
  where: p.published == true,
  preload: [posts: p]
```

### Transactions

```elixir
Repo.transaction(fn ->
  with {:ok, user} <- create_user(params),
       {:ok, profile} <- create_profile(user),
       {:ok, _settings} <- create_settings(user) do
    user
  else
    {:error, reason} -> Repo.rollback(reason)
  end
end)
```

## Testing with ExUnit

```elixir
defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  describe "create_user/1" do
    test "creates user with valid attributes" do
      attrs = %{name: "John", email: "john@example.com"}
      assert {:ok, user} = Accounts.create_user(attrs)
      assert user.name == "John"
    end

    test "returns error with invalid email" do
      attrs = %{name: "John", email: "invalid"}
      assert {:error, changeset} = Accounts.create_user(attrs)
      assert %{email: ["has invalid format"]} = errors_on(changeset)
    end
  end
end

# Testing LiveView
defmodule MyAppWeb.CounterLiveTest do
  use MyAppWeb.ConnCase
  import Phoenix.LiveViewTest

  test "increments counter", %{conn: conn} do
    {:ok, view, _html} = live(conn, "/counter")
    assert view |> element("button") |> render_click() =~ "Count: 1"
  end
end
```

## Deployment Best Practices

### Releases

Use Elixir releases for production:

```elixir
# mix.exs
def project do
  [
    releases: [
      myapp: [
        include_executables_for: [:unix],
        steps: [:assemble, :tar]
      ]
    ]
  ]
end
```

Build and deploy:

```bash
MIX_ENV=prod mix release
_build/prod/rel/myapp/bin/myapp start
```

### Configuration

```elixir
# config/runtime.exs
import Config

if config_env() == :prod do
  database_url = System.get_env("DATABASE_URL") ||
    raise "DATABASE_URL not available"

  config :myapp, MyApp.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
end
```

### Health Checks

```elixir
# In your router
get "/health", HealthController, :check

# Controller
def check(conn, _params) do
  case Repo.query("SELECT 1") do
    {:ok, _} -> send_resp(conn, 200, "ok")
    _ -> send_resp(conn, 503, "database unavailable")
  end
end
```

## Consolidated Skills

This expert skill consolidates 1 individual skills:

- elixir-expert

## Iron Laws

1. **ALWAYS** use pattern matching and guards for control flow instead of nested if/case — idiomatic Elixir communicates intent through pattern matching; imperative conditionals fight the language.
2. **NEVER** use shared mutable state — always communicate through message passing between processes; shared mutable state in Elixir requires explicit ETS or Agent, which should be the exception not the rule.
3. **ALWAYS** use supervision trees for fault tolerance — never spawn bare processes that aren't supervised; unsupervised processes crash silently without recovery.
4. **NEVER** use `Enum` functions on potentially large streams — use `Stream` for lazy evaluation to avoid loading entire collections into memory.
5. **ALWAYS** write doctests (`iex>` examples in `@doc`) for public functions — doctests are runnable specifications; they document behavior and serve as regression tests.

## Anti-Patterns

| Anti-Pattern                                      | Why It Fails                                                                  | Correct Approach                                                            |
| ------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Nested if/cond chains instead of pattern matching | Harder to read; misses the power of Elixir's pattern matching; doesn't scale  | Use function clauses with pattern matching heads and guard clauses          |
| Bare `spawn` without supervision                  | Crashed processes disappear silently; no restart, no visibility               | Always use `Supervisor` trees; use `Task.Supervisor` for dynamic tasks      |
| `Enum.map/filter` on large streams                | Loads entire collection into memory; causes OOM on large datasets             | Use `Stream.map/filter` for lazy, memory-efficient pipeline processing      |
| Global mutable state via Process dictionary       | Process dictionary is implicit state; makes code unpredictable and untestable | Use `Agent`, `GenServer`, or `ETS` explicitly when shared state is required |
| No doctests for public functions                  | Public API has no runnable specification; behavior drifts from documentation  | Always add `iex>` examples in `@doc`; run with `mix test`                   |

## Memory Protocol (MANDATORY)

**Before starting:**

```bash
cat .claude/context/memory/learnings.md
```

**After completing:** Record any new patterns or exceptions discovered.

> ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.

