Use when writing Ecto code. Contains insights about Contexts, DDD patterns, schemas, changesets, and database gotchas from José Valim.
/plugin marketplace add georgeguimaraes/claude-code-elixir/plugin install georgeguimaraes-elixir-plugins-elixir@georgeguimaraes/claude-code-elixirThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Mental shifts for Ecto and data layer design. These insights challenge typical ORM patterns.
Context isn't just a namespace—it changes what words mean:
| Subdomain | What "Product" Means |
|---|---|
| Checkout | SKU, name, description |
| Billing | SKU, quantity, cost per unit |
| Fulfillment | SKU, label, quantity on hand, warehouse |
"The product you thought was a single concept is not the same across subdomains."
Each bounded context may have its OWN Product schema/table.
Listen to domain experts. Same language, different dialects:
Checkout team: "Customer selects products"
Billing team: "We calculate line items"
Fulfillment team: "We ship packages"
Each group uses "product" differently → separate bounded contexts.
Wrong: Entity → Context (database-driven design) Right: Subdomain → Context → Entity
Stop asking "What context does Product belong to?" Start asking "What is a Product in this business domain?"
Apply patterns like Plug conn or Ecto changeset:
def create_product(params) do
params
|> Products.build() # Factory: unstructured → domain
|> Products.validate() # Aggregate: enforce invariants
|> Products.insert() # Repository: persist
end
| Pattern | Elixir Equivalent |
|---|---|
| Entity | Schema with ID |
| Value Object | Embedded schema, struct |
| Aggregate | Module with transactional operations |
| Factory | build/1 functions |
| Repository | get/1, insert/1 wrappers |
Think in events → commands:
| Event (Past) | Command (Context Function) |
|---|---|
| Order form submitted | Checkout.place_order/1 |
| Blog post created | Social.notify_followers/1 |
"Listen to the domain language. Express domain behaviors as functions."
Use events as data structures (not event sourcing machinery):
def agent_joins_room(agent_id) do
with {:ok, room} <- BackgroundCalls.get_available_room(),
{:ok, event} <- BackgroundCalls.start_dialing_session(room, agent_id) do
# Event struct passed to another bounded context
TaskManagement.update_task(event)
end
end
Why events: Minimal data shared, reduces coupling, contexts stay autonomous.
defmodule ShoppingCart.CartItem do
schema "cart_items" do
field :product_id, :integer # Reference by ID
# NOT: belongs_to :product, Catalog.Product
end
end
# Query through the context
def get_cart_with_products(cart) do
product_ids = Enum.map(cart.items, & &1.product_id)
products = Catalog.list_products_by_ids(product_ids)
end
Keeps contexts independent and testable.
Ecto schemas are Elixir representations—they don't have to map 1:1:
| Use Case | Approach |
|---|---|
| Database table | Standard schema/2 |
| Form validation only | embedded_schema/1 |
| API request/response | Embedded schema or schemaless |
| JSON column structure | embeds_one/many |
def registration_changeset(user, attrs) # Full validation + password
def profile_changeset(user, attrs) # Name, bio only
def admin_changeset(user, attrs) # Role, verified_at
Different operations = different changesets.
create table(:comments) do
add :org_id, :integer, null: false
add :post_id, references(:posts, with: [org_id: :org_id], match: :full)
end
Use prepare_query/3 for automatic scoping:
def prepare_query(_operation, query, opts) do
if org_id = opts[:org_id] do
{from(q in query, where: q.org_id == ^org_id), opts}
else
raise "org_id is required"
end
end
"If you have a CRUD bounded context, go for it. No need to add complexity."
Use generators for simple cases. Only add factory/aggregate/repository when business logic demands it.
| Approach | Best For |
|---|---|
| Separate preloads | Has-many with many records (less memory) |
| Join preloads | Belongs-to, has-one (single query) |
Join preloads can use 10x more memory for has-many.
Counterintuitive behaviors documented by José Valim.
In multi-tenant apps, CTEs don't get the parent query's prefix.
# The CTE runs in wrong schema:
from(p in Product)
|> with_cte("tree", as: ^recursive_query) # recursive_query has no prefix!
Fix: Explicitly set prefix: %{recursive_query | prefix: "tenant"}
Changeset.cast mixed keys warning only checks first 32 keys. Atom keys after position 32 slip through.
params = %{"k1" => 1, ..., "k32" => 32, key33: 33} # No warning!
Why: Performance—full check appeared in profiling.
Fix: Sanitize params at controller boundary, not changeset.
Empty assocs/embeds populate based on whether struct is loaded from DB, not whether primary key exists.
user = %User{id: 1} # Manually set ID
put_assoc(user, :posts, []) # Doesn't work as expected
user = Repo.get!(User, 1) # Loaded from DB
put_assoc(user, :posts, []) # Works - struct is "loaded"
Fix: Use Repo.load/2 if you need a "loaded" struct without DB query.
Repo.transaction is being soft-deprecated. New transact only allows {:ok, _} or {:error, _} returns.
# Old - ambiguous what triggers rollback:
Repo.transaction(fn ->
:something # Does this rollback? Who knows!
end)
# New - explicit:
Repo.transact(fn ->
{:ok, result} # or {:error, reason}
end)
Plan for this API change.
Instead of sorting after fetch:
schema "posts" do
has_many :comments, Comment, preload_order: [desc: :inserted_at]
end
Note: Doesn't work for through associations—sort those after fetching.
"Those are separated things. One is parameterized queries the other is prepared statements."
SELECT * FROM users WHERE id = $1 — always used by Ectopgbouncer compatibility: Use prepare: :unnamed (disables prepared statements, keeps parameterized queries).
More pools with fewer connections = better for benchmarks:
| pool_count | pool_size | ips |
|---|---|---|
| 1 | 32 | 1.53 K |
| 8 | 4 | 2.39 K |
But: With heterogeneous workflows (mix of fast/slow queries), a single larger pool gives better latency—you get "first available connection."
Rule: pool_count for uniform workloads (benchmarks), larger pool_size for real apps.
Cachex, separate GenServers, or anything outside the test process won't share the sandbox transaction.
"If they need to be part of the same transaction, then you need to redesign the solution because they are not part of the same transaction in practice and the sandbox helped you find a bug."
Fix: Make the external service use the test process, or disable sandbox for those tests.
PostgreSQL rejects null bytes even though they're valid UTF-8:
Repo.insert(%User{name: "foo\x00bar"}) # Raises!
Fix: Sanitize at boundaries: String.replace(string, "\x00", "")
For migrations at runtime (not mix tasks):
Ecto.Migrator.run(Repo, [{0, MyApp.Migration1}, {1, MyApp.Migration2}], :up, opts)
Keeps migrations as compiled code, avoids module recompilation warnings.
| Excuse | Reality |
|---|---|
| "This app is too small for contexts" | Contexts are about meaning, not size. |
| "I'll just use belongs_to across contexts" | Cross-context = IDs only. Keeps contexts independent. |
| "One schema per table is cleaner" | Multiple schemas per table is valid. Different views = different schemas. |
| "Preloading everything is easier" | Join preloads can use 10x memory. Think about it. |
| "I'll add contexts later" | Refactoring contexts is painful. Design upfront. |
| "CRUD doesn't need DDD" | CRUD contexts are fine. DDD is optional complexity. |
prepare: :unnamedAny of these? Re-read the Gotchas section.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.