Agent skill
ecto-patterns
Ecto patterns for Phoenix/Elixir apps. Covers schemas, changesets, migrations, queries, Ecto.Multi, transactions, constraints, associations, pagination, tenant partitioning, performance, and testing.
Install this agent skill to your Project
npx add-skill https://github.com/bobmatnyc/claude-mpm-skills/tree/main/toolchains/elixir/data/ecto-patterns
SKILL.md
Ecto Patterns for Phoenix/Elixir
Ecto is the data layer for Phoenix applications: schemas, changesets, queries, migrations, and transactions. Good Ecto practice keeps domain logic in contexts, enforces constraints in the database, and uses transactions for multi-step workflows.
Schemas and Changesets
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :hashed_password, :string
field :confirmed_at, :naive_datetime
has_many :memberships, MyApp.Orgs.Membership
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 12)
|> unique_constraint(:email)
|> hash_password()
end
defp hash_password(%{valid?: true} = cs),
do: put_change(cs, :hashed_password, Argon2.hash_pwd_salt(get_change(cs, :password)))
defp hash_password(cs), do: cs
end
Guidelines
- Keep casting/validation in changesets; keep business logic in contexts.
- Always pair validation with DB constraints (
unique_constraint,foreign_key_constraint). - Use
changeset/2for updates; avoid mass assigning without casting.
Migrations
def change do
create table(:users) do
add :email, :citext, null: false
add :hashed_password, :string, null: false
add :confirmed_at, :naive_datetime
timestamps()
end
create unique_index(:users, [:email])
end
Safe migration tips
- Prefer additive changes: add columns nullable, backfill, then enforce null: false.
- For large tables: use
concurrently: truefor indexes; disable inchangeand wrap inup/downfor Postgres. - Data migrations belong in separate modules called from
mix ecto.migrateviaexecute/1or in distinct scripts; ensure idempotence. - Coordinate locks: avoid long transactions; break migrations into small steps.
Queries and Preloads
import Ecto.Query
def list_users(opts \\ %{}) do
base =
from u in MyApp.Accounts.User,
preload: [:memberships],
order_by: [desc: u.inserted_at]
Repo.all(apply_pagination(base, opts))
end
defp apply_pagination(query, %{limit: limit, offset: offset}),
do: query |> limit(^limit) |> offset(^offset)
defp apply_pagination(query, _), do: query
Patterns
- Use
preloadrather than calling Repo in loops; preferRepo.preload/2after fetching. - Use
selectto avoid loading large blobs. - For concurrency, use
Repo.transactionwithlock: "FOR UPDATE"in queries that need row-level locks.
Transactions and Ecto.Multi
alias Ecto.Multi
def onboard_user(attrs) do
Multi.new()
|> Multi.insert(:user, User.registration_changeset(%User{}, attrs))
|> Multi.insert(:org, fn %{user: user} ->
Org.changeset(%Org{}, %{owner_id: user.id, name: attrs["org_name"]})
end)
|> Multi.run(:welcome, fn _repo, %{user: user} ->
MyApp.Mailer.deliver_welcome(user)
{:ok, :sent}
end)
|> Repo.transaction()
end
Guidelines
- Prefer
Multi.run/3for side effects that can fail; return{:ok, value}or{:error, reason}. - Use
Multi.update_allfor batch updates; includewhereguards to prevent unbounded writes. - Propagate errors upward; translate them in controllers/LiveViews.
Associations and Constraints
- Use
on_replace: :delete/:nilifyto control nested changes. - Define
foreign_key_constraint/3andunique_constraint/3in changesets to surface DB errors cleanly. - For many-to-many, prefer join schema (
has_many :memberships) instead of automaticmany_to_manywhen you need metadata.
Pagination and Filtering
- Offset/limit for small datasets; cursor-based for large lists (
Scrivener,Flop,Paginator). - Normalize filters in contexts; avoid letting controllers build queries directly.
- Add composite indexes to match filter columns; verify with
EXPLAIN ANALYZE.
Multi-Tenancy Patterns
- Prefix-based: Postgres schemas per tenant (
put_source/2withprefix:) — good isolation, needs per-tenant migrations. - Row-based:
tenant_idcolumn + row filters — simpler migrations; add partial indexes per tenant when large. - Always scope queries by tenant in contexts; consider using policies/guards to enforce.
Performance and Ops
- Use
Repo.streamfor large exports; wrap inRepo.transaction. - Cache hot reads with ETS/Cachex; invalidate on writes.
- Watch query counts in LiveView/Channels; preload before rendering to avoid N+1.
- Telemetry:
OpentelemetryEctoexports query timings; add DB connection pool metrics.
Testing
use MyApp.DataCase, async: true
test "registration changeset validates email" do
changeset = User.registration_changeset(%User{}, %{email: "bad", password: "secretsecret"})
refute changeset.valid?
assert %{email: ["has invalid format"]} = errors_on(changeset)
end
DataCasesets up sandboxed DB; keep tests async unless transactions conflict.- Use factories/fixtures in
test/supportto build valid structs quickly. - For migrations, add regression tests for constraints (unique/index-backed constraints).
Common Pitfalls
- Running risky DDL in a single migration step (avoid locks; break apart).
- Skipping DB constraints and relying only on changesets.
- Querying associations in loops instead of preloading.
- Missing transactions for multi-step writes (partial state on failure).
- Forgetting tenant scoping on read/write in multi-tenant setups.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
bad-example-skill
ANTI-PATTERN - Example showing violations of self-containment (DO NOT COPY)
example-framework-skill
Example of a properly self-contained skill following all best practices
systematic-debugging
Systematic debugging methodology emphasizing root cause analysis over quick fixes
verification-before-completion
Run verification commands and confirm output before claiming success
Root Cause Tracing
Systematically trace bugs backward through call stack to find original trigger
Brainstorming Ideas Into Designs
Interactive idea refinement using Socratic method to develop fully-formed designs
Didn't find tool you were looking for?