elixir

Thinking skills for Elixir, Phoenix, Ecto, OTP, and Oban patterns

View on GitHub
Author George Guimaraes
Namespace @georgeguimaraes/claude-code-elixir
Category skills
Version 1.0.0
Stars 82
Downloads 3
self.md verified
Table of content

Thinking skills for Elixir, Phoenix, Ecto, OTP, and Oban patterns

Installation

npx claude-plugins install @georgeguimaraes/claude-code-elixir/elixir

Contents

Folders: hooks, skills

Included Skills

This plugin includes 5 skill definitions:

ecto-thinking

This skill should be used when the user asks to “add a database table”, “create a new context”, “query the database”, “add a field to a schema”, “validate form input”, “fix N+1 queries”, “preload this association”, “separate these concerns”, or mentions Repo, changesets, migrations, Ecto.Multi, has_many, belongs_to, transactions, query composition, or how contexts should talk to each other.

View skill definition

Ecto Thinking

Mental shifts for Ecto and data layer design. These insights challenge typical ORM patterns.

Context = Setting That Changes Meaning

Context isn’t just a namespace—it changes what words mean. “Product” means different things in Checkout (SKU, name), Billing (SKU, cost), and Fulfillment (SKU, warehouse). Each bounded context may have its OWN Product schema/table.

Think top-down: Subdomain → Context → Entity. Not “What context does Product belong to?” but “What is a Product in this business domain?”

Cross-Context References: IDs, Not Associations

schema "cart_items" do
  field :product_id, :integer  # Reference by ID
  # NOT: belongs_to :product, Catalog.Product
end

Query through the context, not across associations. Keeps contexts independent and testable.

DDD Patterns as Pipelines

def create_product(params) do
  params
  |> Products.build()       # Factory: unstructured → domain
  |> Products.validate()    # Aggregate: enforce invariants
  |> Products.insert()      # Repository: persist
end

Use events (as data structs) to compose bounded contexts with minimal coupling.

Schema ≠ Database Table

Use CaseApproach
Database tableStandard schema/2
Form validation onlyembedded_schema/1
API request/responseEmbedded schema or schemaless

Multiple Changesets per Schema

def registration_changeset(user, attrs)  # Full validation + password
def profil

...(truncated)

</details>

### elixir-thinking

> This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours, pattern matching, with statements, comprehensions, structs, or coming from an OOP background. Contains paradigm-shifting insights.

<details>
<summary>View skill definition</summary>

# Elixir Thinking

Mental shifts required before writing Elixir. These contradict conventional OOP patterns.

## The Iron Law

NO PROCESS WITHOUT A RUNTIME REASON


Before creating a GenServer, Agent, or any process, answer YES to at least one:
1. Do I need mutable state persisting across calls?
2. Do I need concurrent execution?
3. Do I need fault isolation?

**All three are NO?** Use plain functions. Modules organize code; processes manage runtime.

## The Three Decoupled Dimensions

OOP couples behavior, state, and mutability together. Elixir decouples them:

| OOP Dimension | Elixir Equivalent |
|---------------|-------------------|
| Behavior | Modules (functions) |
| State | Data (structs, maps) |
| Mutability | Processes (GenServer) |

Pick only what you need. "I only need data and functions" = no process needed.

## "Let It Crash" = "Let It Heal"

The misconception: Write careless code.
The truth: Supervisors START processes.

- Handle expected errors explicitly (`{:ok, _}` / `{:error, _}`)
- Let unexpected errors crash  supervisor restarts

## Control Flow

**Pattern matching first:**
- Match on function heads instead of `if/else` or `case` in bodies
- `%{}` matches ANY mapuse `map_size(map) == 0` guard for empty maps
- Avoid nested `case`refactor to single `case`, `with`, or separate functions

**Error handling:**
- Use `{:ok, result}` / `{:error, reason}` for operations that can fail
- Avoid raising exceptions for control flow
- Use `with` for chaining `{

...(truncated)

</details>

### oban-thinking

> This skill should be used when the user asks to "add a background job", "process async", "schedule a task", "retry failed jobs", "add email sending", "run this later", "add a cron job", "unique jobs", "batch process", or mentions Oban, Oban Pro, workflows, job queues, cascades, grafting, recorded values, job args, or troubleshooting job failures.

<details>
<summary>View skill definition</summary>

# Oban Thinking

Paradigm shifts for Oban job processing. These insights prevent common bugs and guide proper patterns.

---

# Part 1: Oban (Non-Pro)

## The Iron Law: JSON Serialization

JOB ARGS ARE JSON. ATOMS BECOME STRINGS.


This single fact causes most Oban debugging headaches.

```elixir
# Creating - atom keys are fine
MyWorker.new(%{user_id: 123})

# Processing - must use string keys (JSON converted atoms to strings)
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
  # ...
end

Error Handling: Let It Crash

Don’t catch errors in Oban jobs. Let them bubble up to Oban for proper handling.

Why?

  1. Automatic logging: Oban logs the full error with stacktrace
  2. Automatic retries: Jobs retry with exponential backoff
  3. Visibility: Failed jobs appear in Oban Web dashboard
  4. Consistency: Error states are tracked in the database

Anti-Pattern

# Bad: Swallowing errors
def perform(%Oban.Job{} = job) do
  case do_work(job.args) do
    {:ok, result} -> {:ok, result}
    {:error, reason} ->
      Logger.error("Failed: #{reason}")
      {:ok, :failed}  # Silently marks as complete!
  end
end

Correct Pattern

# Good: Let errors propagate
def perform(%Oban.Job{} = job) do
  result = do_work!(job.args)  # Raises on failure
  {:ok, result}
end

# Or return error tuple - Oban treats as failure
def perform(%Oban.Job{} = job) do
  case do_work(job.args) do
    {:ok, result} -> {:ok, result}
    {:error, re

...(truncated)

</details>

### otp-thinking

> This skill should be used when the user asks to "add background processing", "cache this data", "run this async", "handle concurrent requests", "manage state across requests", "process jobs from a queue", "this GenServer is slow", or mentions GenServer, Supervisor, Agent, Task, Registry, DynamicSupervisor, handle_call, handle_cast, supervision trees, fault tolerance, "let it crash", or choosing between Broadway and Oban.

<details>
<summary>View skill definition</summary>

# OTP Thinking

Paradigm shifts for OTP design. These insights challenge typical concurrency and state management patterns.

## The Iron Law

GENSERVER IS A BOTTLENECK BY DESIGN


A GenServer processes ONE message at a time. Before creating one, ask:
1. Do I actually need serialized access?
2. Will this become a throughput bottleneck?
3. Can reads bypass the GenServer via ETS?

**The ETS pattern:** GenServer owns ETS table, writes serialize through GenServer, reads bypass it entirely with `:read_concurrency`.

**No exceptions:** Don't wrap stateless functions in GenServer. Don't create GenServer "for organization".

## GenServer Patterns

| Function | Use For |
|----------|---------|
| `call/3` | Synchronous requests expecting replies |
| `cast/2` | Fire-and-forget messages |

**When in doubt, use `call`** to ensure back-pressure. Set appropriate timeouts for `call/3`.

Use `handle_continue/2` for post-init workkeeps `init/1` fast and non-blocking.

## Task.Supervisor, Not Task.async

`Task.async` spawns a **linked** processif task crashes, caller crashes too.

| Pattern | On task crash |
|---------|---------------|
| `Task.async/1` | Caller crashes (linked, unsupervised) |
| `Task.Supervisor.async/2` | Caller crashes (linked, supervised) |
| `Task.Supervisor.async_nolink/2` | Caller survives, can handle error |

**Use Task.Supervisor for:** Production code, graceful shutdown, observability, `async_nolink`.
**Use Task.async for:** Quick experiments, scripts, when cra

...(truncated)

</details>

### phoenix-thinking

> This skill should be used when the user asks to "add a LiveView page", "create a form", "handle real-time updates", "broadcast changes to users", "add a new route", "create an API endpoint", "fix this LiveView bug", "why is mount called twice?", or mentions handle_event, handle_info, handle_params, mount, channels, controllers, components, assigns, sockets, or PubSub. Essential for avoiding duplicate queries in mount.

<details>
<summary>View skill definition</summary>

# Phoenix Thinking

Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.

## The Iron Law

NO DATABASE QUERIES IN MOUNT


mount/3 is called TWICE (HTTP request + WebSocket connection). Queries in mount = duplicate queries.

```elixir
def mount(_params, _session, socket) do
  # NO database queries here! Called twice.
  {:ok, assign(socket, posts: [], loading: true)}
end

def handle_params(params, _uri, socket) do
  # Database queries here - once per navigation
  posts = Blog.list_posts(socket.assigns.scope)
  {:noreply, assign(socket, posts: posts, loading: false)}
end

mount/3 = setup only (empty assigns, subscriptions, defaults) handle_params/3 = data loading (all database queries, URL-driven state)

No exceptions: Don’t query “just this one small thing” in mount. Don’t “optimize later”. LiveView lifecycle is non-negotiable.

Scopes: Security-First Pattern (Phoenix 1.8+)

Scopes address OWASP #1 vulnerability: Broken Access Control. Authorization context is threaded automatically—no more forgetting to scope queries.

def list_posts(%Scope{user: user}) do
  Post |> where(user_id: ^user.id) |> Repo.all()
end

PubSub Topics Must Be Scoped

def subscribe(%Scope{organization: org}) do
  Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end

Unscoped topics = data leaks between tenants.

External Polling: GenServer, Not LiveView

Bad: Every connected user makes API ca

…(truncated)

Source

View on GitHub

Tags: skills elixirphoenixectootpobanliveviewgenserver