
Ecto Patterns
- 230 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
Model schemas, queries, changesets, and repo patterns in Elixir Ecto when building Phoenix APIs, background jobs, or data-heavy SaaS backends with PostgreSQL.
About
Covers idiomatic Elixir Ecto patterns for schemas, migrations, changesets, associations, and repository queries so Phoenix and Elixir backends stay consistent, testable, and safe against common database pitfalls.
- Schema and migration design
- Changesets and validations
- Ecto.Repo query patterns
- Associations and preloading
- Transactions and multi-step writes
Ecto Patterns by the numbers
- 230 all-time installs (skills.sh)
- Ranked #212 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill ecto-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 230 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Model schemas, queries, changesets, and repo patterns in Elixir Ecto when building Phoenix APIs, background jobs, or data-heavy SaaS backends with PostgreSQL.
Files
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
endGuidelines
- 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])
endSafe 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: queryPatterns
- 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()
endGuidelines
- 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)
endDataCasesets 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.
{
"name": "ecto-patterns",
"version": "1.0.0",
"category": "toolchain",
"toolchain": "elixir",
"framework": "ecto-patterns",
"tags": [
"elixir",
"ecto",
"phoenix",
"queries",
"migrations",
"transactions",
"multi-tenancy",
"pagination",
"performance",
"testing"
],
"entry_point_tokens": 170,
"full_tokens": 1646,
"author": "Claude MPM Team",
"license": "MIT",
"requires": [],
"updated": "2025-12-05",
"source_path": "toolchains/elixir/data/ecto-patterns/SKILL.md",
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}