boundaries
Analyze Phoenix context boundaries and module coupling via mix xref. Use when checking cross-context calls, validating dependencies, before splitting modules, or reviewing architecture.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/boundaries
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Phoenix Context Boundary Validation
Analyze module dependencies to ensure clean context separation and proper architectural boundaries.
Usage
/phx:boundaries # Check for violations
/phx:boundaries --assess # Score context health (0-100)
/phx:boundaries --fix # Suggest fixes for violations
--assess Mode: Context Health Score
Evaluate overall boundary health with a quantified score.
Metrics Calculated
| Metric | Healthy Range | Red Flag | Weight |
|---|---|---|---|
| Modules per context | 3-15 | >20 or <2 | 20% |
| Public API surface | 5-30 funcs | >40 funcs | 15% |
| Fan-out (contexts called) | 1-4 | >6 | 20% |
| Fan-in (called by contexts) | 1-6 | >10 | 15% |
| Circular dependencies | 0 | >0 | 15% |
| Boundary violations | 0 | >0 | 15% |
Commands for Assessment
Use Glob to count .ex files per context directory under lib/my_app/*/.
Use Grep to count public function definitions per context file under lib/my_app/*.ex.
Run mix xref graph --format stats for dependency analysis.
Run mix xref graph --format cycles --label compile for compile-time circular dependencies.
Output Format
## Context Health Assessment
### Overall Score: 82/100 (Good)
| Context | Modules | API | Fan-Out | Fan-In | Score |
|---------|---------|-----|---------|--------|-------|
| Accounts | 5 | 12 | 2 | 4 | 95 |
| Orders | 18 | 45 | 8 | 3 | 62 |
| Shared | 2 | 8 | 0 | 12 | 78 |
### Issues Found
1. **Orders** - Too large (18 modules, 45 funcs)
- Consider: Extract Fulfillment, Invoicing sub-contexts
2. **Orders** - High fan-out (8 contexts)
- Consider: Review if all dependencies necessary
### Recommendations
- Split Orders into Orders + Fulfillment
- Review Accounts ← Billing dependency
Iron Laws - Never Violate These
- Controllers call only contexts - No direct Repo access from web layer
- Schemas are pure data - No side effects, no Repo calls in schema modules
- Contexts own their schemas - Don't import schemas from other contexts
- Explicit dependencies only - Cross-context calls must be intentional
- DO NOT refactor context boundaries without running
mix xreffirst — Refactoring without dependency data creates new violations; always map the dependency graph before moving modules
Dependency Rules
| Layer | Can Call | Cannot Call |
|---|---|---|
| Controllers | Contexts, Plug, Conn | Repo, Schemas directly |
| LiveViews | Contexts, Components, PubSub | Repo, Schemas directly |
| Contexts | Own schemas, Repo, other contexts | Web layer modules |
| Schemas | Ecto types, validations | Contexts, Repo |
Analysis Commands
Check Compile Dependencies
Run mix xref graph --label compile-connected.
Find What Depends on a Context
Run mix xref graph --sink MyApp.Accounts --label compile.
Find What a Module Calls
Run mix xref callers MyApp.Accounts.get_user!/1.
Check for Circular Dependencies
Run mix xref graph --format cycles --label compile.
Red Flags to Detect
| Issue | Detection Command | Fix |
|---|---|---|
| Repo in web layer | grep -r "Repo\." lib/my_app_web/ |
Move to context |
| Schema with queries | grep -r "import Ecto.Query" lib/my_app/**/schemas/ |
Move queries to context |
| Cross-context schema import | grep -r "alias MyApp.Other.Schema" lib/my_app/ctx/ |
Call context API |
| Business logic in LiveView | grep -r "Repo\.\|Ecto\.Multi" lib/my_app_web/live/ |
Extract to context |
Boundary Verification Process
- Run
mix xref graph --label compile-connectedfor overview - Check for context cross-contamination
- Verify no direct Repo calls from web layer
- Ensure schemas have no side effects
- Validate explicit cross-context dependencies
Next Steps
Always end with actionable follow-up — findings without a plan get lost:
- `/phx:plan` — Create a plan to fix violations (recommended for 3+ issues)
- `/phx:quick` — Fix a single boundary violation directly
- `/phx:review` — Review specific modules for deeper issues
References
For detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/context-design.md- Context design principles${CLAUDE_SKILL_DIR}/references/refactoring-boundaries.md- Fixing boundary violations
Files (claude-elixir-phoenix)
-
references
-
context-design.md 4.4 KB
# Context Design Principles Guidelines for designing and maintaining Phoenix context boundaries. ## Context Responsibilities ### What Belongs in a Context - Business logic and domain rules - Data access (Repo calls) - Schema ownership - Changeset definitions - Transaction coordination - PubSub broadcasting ### What Does NOT Belong in a Context - HTTP concerns (conn, params parsing) - Presentation logic - View helpers - WebSocket handling - External API clients (use separate modules) ## Context API Design ### Public Functions ```elixir defmodule MyApp.Accounts do @moduledoc """ The Accounts context handles user management and authentication. """ # List operations def list_users(opts \\ []) def list_active_users # Get operations (return nil or raise) def get_user(id) def get_user!(id) def get_user_by_email(email) # Create operations def create_user(attrs) def register_user(attrs) # Update operations def update_user(user, attrs) def change_user_email(user, attrs) # Delete operations def delete_user(user) # Changeset functions (for forms) def change_user(user, attrs \\ %{}) end ``` ### Function Naming Conventions | Prefix | Meaning | Returns | |--------|---------|---------| | `list_` | Collection | `[%Schema{}]` | | `get_` | Single item, may not exist | `%Schema{} \| nil` | | `get_!` | Single item, must exist | `%Schema{}` or raises | | `create_` | New record | `{:ok, %Schema{}} \| {:error, changeset}` | | `update_` | Modify record | `{:ok, %Schema{}} \| {:error, changeset}` | | `delete_` | Remove record | `{:ok, %Schema{}} \| {:error, changeset}` | | `change_` | Return changeset | `%Changeset{}` | ## Cross-Context Communication ### Option 1: Direct Function Calls For simple, synchronous operations: ```elixir defmodule MyApp.Orders do alias MyApp.Accounts def create_order(user_id, attrs) do user = Accounts.get_user!(user_id) # ... create order end end ``` ### Option 2: PubSub for Decoupling For events that trigger side effects: ```elixir # In Orders context defmodule MyApp.Orders do def complete_order(order) do with {:ok, order} <- update_order(order, %{status: :completed}) do Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_completed, order}) {:ok, order} end end end # In Notifications context (subscriber) defmodule MyApp.Notifications do def handle_info({:order_completed, order}, state) do send_order_confirmation(order) {:noreply, state} end end ``` ### Option 3: Domain Events For complex workflows: ```elixir defmodule MyApp.Events do def dispatch(%{type: :order_completed} = event) do MyApp.Notifications.handle(event) MyApp.Analytics.handle(event) MyApp.Inventory.handle(event) end end ``` ## Context Boundaries Checklist ### When Creating New Context - [ ] Single responsibility (one bounded context) - [ ] Clear API surface (public functions documented) - [ ] Owns its schemas (no shared schemas) - [ ] No web layer dependencies - [ ] Tests don't require web layer ### When Adding Cross-Context Dependency - [ ] Dependency is intentional (not accidental) - [ ] Using public API (not internal functions) - [ ] No circular dependencies created - [ ] Consider if PubSub is better fit ## Anti-Patterns to Avoid ### Bloated Contexts ```elixir # BAD: One context doing too much defmodule MyApp.Core do def create_user(attrs) def create_order(attrs) def create_product(attrs) def send_email(to, subject, body) def process_payment(amount) end # GOOD: Separate concerns defmodule MyApp.Accounts do ... end defmodule MyApp.Orders do ... end defmodule MyApp.Catalog do ... end defmodule MyApp.Mailer do ... end defmodule MyApp.Payments do ... end ``` ### Leaky Abstractions ```elixir # BAD: Exposing internal query def get_active_users_query do from u in User, where: u.active == true end # GOOD: Return data, not queries def list_active_users do from(u in User, where: u.active == true) |> Repo.all() end ``` ### Schema Sharing ```elixir # BAD: Sharing schemas between contexts defmodule MyApp.Orders do alias MyApp.Accounts.User # Cross-context schema access def create_order(%User{} = user, attrs) do # Tight coupling end end # GOOD: Use IDs, call context API defmodule MyApp.Orders do alias MyApp.Accounts def create_order(user_id, attrs) do user = Accounts.get_user!(user_id) # Create order with user data end end ``` -
refactoring-boundaries.md 4.7 KB
# Refactoring Boundary Violations Step-by-step guide to fixing common Phoenix context boundary issues. ## Fixing Direct Repo Access in Controllers ### Before (Violation) ```elixir defmodule MyAppWeb.UserController do alias MyApp.Repo alias MyApp.Accounts.User def show(conn, %{"id" => id}) do user = Repo.get!(User, id) # Direct Repo access! render(conn, :show, user: user) end end ``` ### After (Fixed) ```elixir # In context defmodule MyApp.Accounts do def get_user!(id), do: Repo.get!(User, id) end # In controller defmodule MyAppWeb.UserController do alias MyApp.Accounts def show(conn, %{"id" => id}) do user = Accounts.get_user!(id) render(conn, :show, user: user) end end ``` ## Fixing Business Logic in LiveView ### Before (Violation) ```elixir defmodule MyAppWeb.OrderLive do def handle_event("complete", %{"id" => id}, socket) do order = Repo.get!(Order, id) # Business logic in LiveView! Ecto.Multi.new() |> Ecto.Multi.update(:order, Order.changeset(order, %{status: :completed})) |> Ecto.Multi.insert(:notification, Notification.changeset(%{...})) |> Repo.transaction() {:noreply, socket} end end ``` ### After (Fixed) ```elixir # In context defmodule MyApp.Orders do def complete_order(order) do Ecto.Multi.new() |> Ecto.Multi.update(:order, Order.changeset(order, %{status: :completed})) |> Ecto.Multi.run(:notification, fn repo, _ -> MyApp.Notifications.create_order_notification(order) end) |> Repo.transaction() end end # In LiveView defmodule MyAppWeb.OrderLive do alias MyApp.Orders def handle_event("complete", %{"id" => id}, socket) do order = Orders.get_order!(id) case Orders.complete_order(order) do {:ok, _} -> {:noreply, put_flash(socket, :info, "Order completed")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed")} end end end ``` ## Fixing Schema with Queries ### Before (Violation) ```elixir defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Query # Violation! schema "users" do field :email, :string end def active_query do from u in __MODULE__, where: u.active == true end end ``` ### After (Fixed) ```elixir # Schema is pure defmodule MyApp.Accounts.User do use Ecto.Schema schema "users" do field :email, :string field :active, :boolean end def changeset(user, attrs) do user |> cast(attrs, [:email, :active]) |> validate_required([:email]) end end # Queries in context defmodule MyApp.Accounts do import Ecto.Query def list_active_users do from(u in User, where: u.active == true) |> Repo.all() end end ``` ## Fixing Cross-Context Schema Access ### Before (Violation) ```elixir defmodule MyApp.Orders do alias MyApp.Accounts.User # Tight coupling! alias MyApp.Orders.Order def create_order(%User{id: user_id, email: email}, attrs) do %Order{} |> Order.changeset(Map.put(attrs, :user_id, user_id)) |> Repo.insert() |> tap(fn {:ok, _} -> send_confirmation(email) end) end end ``` ### After (Fixed) ```elixir defmodule MyApp.Orders do alias MyApp.Accounts alias MyApp.Orders.Order def create_order(user_id, attrs) when is_integer(user_id) do # Fetch needed data through context API user = Accounts.get_user!(user_id) %Order{} |> Order.changeset(Map.put(attrs, :user_id, user_id)) |> Repo.insert() |> tap(fn {:ok, _} -> send_confirmation(user.email) end) end end ``` ## Migration Strategy ### Step 1: Identify Violations ```bash # Find Repo access in web layer grep -r "Repo\." lib/my_app_web/ --include="*.ex" # Find cross-context aliases grep -r "alias MyApp\.\w\+\.\w\+" lib/my_app/ --include="*.ex" # Find import Ecto.Query in schemas grep -r "import Ecto.Query" lib/my_app/**/schemas/ --include="*.ex" ``` ### Step 2: Create Context Functions For each violation, create appropriate context function: | Violation | Solution | |-----------|----------| | `Repo.get(Schema, id)` | `Context.get_schema(id)` | | `Repo.all(Schema)` | `Context.list_schemas()` | | `Repo.insert(changeset)` | `Context.create_schema(attrs)` | | `Ecto.Multi` in controller | `Context.complex_operation(...)` | ### Step 3: Update Callers Replace direct calls with context API calls. ### Step 4: Verify with xref ```bash # After refactoring, verify no web -> Repo dependencies mix xref graph --source lib/my_app_web/ --sink MyApp.Repo # Should return empty or only through contexts ``` ## Incremental Refactoring Tips 1. **Don't refactor everything at once** - Fix one boundary at a time 2. **Add tests first** - Ensure behavior is preserved 3. **Use deprecation warnings** - Mark old functions as deprecated before removing 4. **Keep commits atomic** - One boundary fix per commit
-
-
SKILL.md 4.5 KB
--- name: boundaries description: Analyze Phoenix context boundaries and module coupling via mix xref. Use when checking cross-context calls, validating dependencies, before splitting modules, or reviewing architecture. effort: medium argument-hint: "[--assess|--fix]" --- # Phoenix Context Boundary Validation Analyze module dependencies to ensure clean context separation and proper architectural boundaries. ## Usage ``` /phx:boundaries # Check for violations /phx:boundaries --assess # Score context health (0-100) /phx:boundaries --fix # Suggest fixes for violations ``` ## `--assess` Mode: Context Health Score Evaluate overall boundary health with a quantified score. ### Metrics Calculated | Metric | Healthy Range | Red Flag | Weight | |--------|---------------|----------|--------| | Modules per context | 3-15 | >20 or <2 | 20% | | Public API surface | 5-30 funcs | >40 funcs | 15% | | Fan-out (contexts called) | 1-4 | >6 | 20% | | Fan-in (called by contexts) | 1-6 | >10 | 15% | | Circular dependencies | 0 | >0 | 15% | | Boundary violations | 0 | >0 | 15% | ### Commands for Assessment Use Glob to count `.ex` files per context directory under `lib/my_app/*/`. Use Grep to count public function definitions per context file under `lib/my_app/*.ex`. Run `mix xref graph --format stats` for dependency analysis. Run `mix xref graph --format cycles --label compile` for compile-time circular dependencies. ### Output Format ```markdown ## Context Health Assessment ### Overall Score: 82/100 (Good) | Context | Modules | API | Fan-Out | Fan-In | Score | |---------|---------|-----|---------|--------|-------| | Accounts | 5 | 12 | 2 | 4 | 95 | | Orders | 18 | 45 | 8 | 3 | 62 | | Shared | 2 | 8 | 0 | 12 | 78 | ### Issues Found 1. **Orders** - Too large (18 modules, 45 funcs) - Consider: Extract Fulfillment, Invoicing sub-contexts 2. **Orders** - High fan-out (8 contexts) - Consider: Review if all dependencies necessary ### Recommendations - Split Orders into Orders + Fulfillment - Review Accounts ← Billing dependency ``` ## Iron Laws - Never Violate These 1. **Controllers call only contexts** - No direct Repo access from web layer 2. **Schemas are pure data** - No side effects, no Repo calls in schema modules 3. **Contexts own their schemas** - Don't import schemas from other contexts 4. **Explicit dependencies only** - Cross-context calls must be intentional 5. **DO NOT refactor context boundaries without running `mix xref` first** — Refactoring without dependency data creates new violations; always map the dependency graph before moving modules ## Dependency Rules | Layer | Can Call | Cannot Call | |-------|----------|-------------| | Controllers | Contexts, Plug, Conn | Repo, Schemas directly | | LiveViews | Contexts, Components, PubSub | Repo, Schemas directly | | Contexts | Own schemas, Repo, other contexts | Web layer modules | | Schemas | Ecto types, validations | Contexts, Repo | ## Analysis Commands ### Check Compile Dependencies Run `mix xref graph --label compile-connected`. ### Find What Depends on a Context Run `mix xref graph --sink MyApp.Accounts --label compile`. ### Find What a Module Calls Run `mix xref callers MyApp.Accounts.get_user!/1`. ### Check for Circular Dependencies Run `mix xref graph --format cycles --label compile`. ## Red Flags to Detect | Issue | Detection Command | Fix | |-------|------------------|-----| | Repo in web layer | `grep -r "Repo\." lib/my_app_web/` | Move to context | | Schema with queries | `grep -r "import Ecto.Query" lib/my_app/**/schemas/` | Move queries to context | | Cross-context schema import | `grep -r "alias MyApp.Other.Schema" lib/my_app/ctx/` | Call context API | | Business logic in LiveView | `grep -r "Repo\.\|Ecto\.Multi" lib/my_app_web/live/` | Extract to context | ## Boundary Verification Process 1. Run `mix xref graph --label compile-connected` for overview 2. Check for context cross-contamination 3. Verify no direct Repo calls from web layer 4. Ensure schemas have no side effects 5. Validate explicit cross-context dependencies ## Next Steps Always end with actionable follow-up — findings without a plan get lost: ``` - `/phx:plan` — Create a plan to fix violations (recommended for 3+ issues) - `/phx:quick` — Fix a single boundary violation directly - `/phx:review` — Review specific modules for deeper issues ``` ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/context-design.md` - Context design principles - `${CLAUDE_SKILL_DIR}/references/refactoring-boundaries.md` - Fixing boundary violations
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.