Elixir Ash Framework Mastery: Declarative Application Development

Goal

After completing these projects, you will deeply understand declarative application development in Elixir using the Ash Framework. You will internalize how resources, actions, and data layers work together to eliminate boilerplate while maintaining full control over your domain logic. You will master the art of defining complex business rules through policies, calculations, and aggregates rather than imperative code. Most importantly, you will understand why declarative frameworks fundamentally change how we think about application architecture, enabling you to build production-ready applications with authentication, authorization, and multiple API interfaces in a fraction of the time traditional approaches require.


Why Elixir Ash Framework Matters

The Problem Ash Solves

Traditional web application development follows a repetitive pattern: define a schema, write migrations, create context modules with CRUD functions, add validation logic, implement authorization checks, build API endpoints, and repeat for every resource. This leads to:

  • Boilerplate explosion: 80% of code is structural, not business logic
  • Inconsistent patterns: Different developers implement similar concepts differently
  • Authorization scattered everywhere: Permission checks spread across controllers, contexts, and views
  • API generation tedium: REST and GraphQL endpoints require separate implementations
  • Testing complexity: Each layer requires separate test coverage

The Declarative Revolution

Ash represents a paradigm shift from “tell the computer what to do” to “describe what you want.” Instead of writing imperative code, you declare your domain model, and Ash generates the implementation.

Traditional Approach                    Ash Declarative Approach
┌─────────────────────────┐            ┌─────────────────────────┐
│ Schema Module           │            │ Resource Definition     │
│ ├── Ecto.Schema         │            │ ├── Attributes          │
│ ├── changeset/2         │            │ ├── Relationships       │
│ └── validations         │            │ ├── Actions             │
├─────────────────────────┤            │ ├── Calculations        │
│ Context Module          │            │ ├── Aggregates          │
│ ├── create_*/2          │            │ ├── Policies            │
│ ├── get_*/1             │            │ └── Validations         │
│ ├── update_*/2          │            └──────────┬──────────────┘
│ ├── delete_*/1          │                       │
│ └── list_*/0            │                       ▼
├─────────────────────────┤            ┌─────────────────────────┐
│ Controller/LiveView     │            │ Ash Generates:          │
│ ├── authorization       │            │ ├── CRUD operations     │
│ ├── parameter handling  │            │ ├── Authorization       │
│ └── API serialization   │            │ ├── Validation          │
├─────────────────────────┤            │ ├── JSON:API endpoints  │
│ Authorization Module    │            │ ├── GraphQL schema      │
│ ├── can_*/2 functions   │            │ └── Form handling       │
│ └── policy checks       │            └─────────────────────────┘
└─────────────────────────┘

~500 lines per resource                ~80 lines per resource
Multiple files to maintain             Single source of truth

Real-World Impact

Companies using Ash Framework report:

  • 60-80% reduction in boilerplate code
  • Consistent API behavior across all resources
  • Authorization logic co-located with resource definitions
  • Automatic API generation (REST, GraphQL) from single source
  • Built-in introspection for documentation and tooling

The Ash Ecosystem

                        ┌─────────────────────────────────┐
                        │         Your Application        │
                        │    (Domain Logic & Resources)   │
                        └─────────────┬───────────────────┘
                                      │
                        ┌─────────────▼───────────────────┐
                        │           Ash Core              │
                        │  Resources │ Actions │ Domains  │
                        └─────────────┬───────────────────┘
                                      │
         ┌────────────────────────────┼────────────────────────────┐
         │                            │                            │
         ▼                            ▼                            ▼
┌─────────────────┐          ┌─────────────────┐          ┌─────────────────┐
│   Data Layers   │          │   Extensions    │          │   Interfaces    │
├─────────────────┤          ├─────────────────┤          ├─────────────────┤
│ AshPostgres     │          │ AshAuthentication│         │ AshPhoenix      │
│ AshSqlite       │          │ AshStateMachine │          │ AshJsonApi      │
│ AshCubDB        │          │ AshPaperTrail   │          │ AshGraphQL      │
│ Ash.DataLayer.Ets│         │ AshArchival     │          │ AshAdmin        │
│ Ash.DataLayer.Mnesia│      │ AshOban         │          └─────────────────┘
└─────────────────┘          │ AshAI           │
                             └─────────────────┘

Who Uses Ash

  • Startups: Rapid prototyping with production-ready architecture
  • Enterprises: Consistent patterns across large teams
  • Agencies: Faster client delivery with maintainable code
  • Open Source: Growing community with active development

Prerequisites & Background Knowledge

Essential Prerequisites

Before starting these projects, you should be comfortable with:

  1. Elixir fundamentals (pattern matching, processes, GenServers)
  2. Phoenix basics (controllers, LiveView, contexts)
  3. Ecto fundamentals (schemas, changesets, queries)
  4. SQL basics (tables, relationships, queries)

Self-Assessment Questions

Answer these before starting. If you struggle, review the prerequisite topics first:

  1. What happens when you call GenServer.call/3 vs GenServer.cast/2?
  2. How do Ecto changesets validate data?
  3. What’s the difference between has_many and belongs_to in Ecto?
  4. How does Phoenix.PubSub enable real-time features?
  5. What are LiveView assigns and how do they trigger re-renders?

Development Environment

You’ll need:

  • Elixir 1.15+ (1.17 recommended)
  • Erlang/OTP 26+ (27 recommended)
  • PostgreSQL 14+
  • Phoenix 1.7+
  • A code editor with Elixir support (VS Code with ElixirLS recommended)

Time Investment

  • Quick exploration: 2-3 weeks (Projects 1-8)
  • Solid foundation: 6-8 weeks (Projects 1-15)
  • Full mastery: 12-16 weeks (All projects)

Core Concept Analysis

1. Resources: The Heart of Ash

Resources are the fundamental building blocks in Ash. They define your domain entities including their attributes, relationships, actions, and policies.

┌─────────────────────────────────────────────────────────────┐
│                      Ash Resource                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │ Attributes  │    │ Relationships│   │  Actions    │     │
│  ├─────────────┤    ├─────────────┤    ├─────────────┤     │
│  │ :id         │    │ belongs_to  │    │ :create     │     │
│  │ :title      │    │ has_many    │    │ :read       │     │
│  │ :status     │    │ has_one     │    │ :update     │     │
│  │ :inserted_at│    │ many_to_many│    │ :destroy    │     │
│  └─────────────┘    └─────────────┘    │ :custom_*   │     │
│                                        └─────────────┘     │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │Calculations │    │ Aggregates  │    │  Policies   │     │
│  ├─────────────┤    ├─────────────┤    ├─────────────┤     │
│  │ :full_name  │    │ :count      │    │ authorize_if│     │
│  │ :age        │    │ :sum        │    │ forbid_if   │     │
│  │ :formatted  │    │ :first      │    │ bypass      │     │
│  └─────────────┘    │ :list       │    └─────────────┘     │
│                     └─────────────┘                        │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   Data Layer                         │   │
│  │  (Postgres, SQLite, ETS, Mnesia, Custom...)         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. Actions: Defining Behavior

Actions define what operations can be performed on a resource. Unlike traditional CRUD, Ash encourages semantic action names that reflect business intent.

┌──────────────────────────────────────────────────────────────────┐
│                        Action Types                               │
├────────────────┬────────────────┬────────────────┬───────────────┤
│    :create     │    :read       │    :update     │   :destroy    │
├────────────────┼────────────────┼────────────────┼───────────────┤
│ Creates new    │ Queries data   │ Modifies       │ Removes       │
│ records        │ with filters   │ existing       │ records       │
│                │ and sorts      │ records        │               │
├────────────────┴────────────────┴────────────────┴───────────────┤
│                                                                   │
│  Semantic Actions (Recommended):                                  │
│                                                                   │
│  Instead of:              Use:                                    │
│  ├── :create          →   :register, :submit, :draft             │
│  ├── :read            →   :list_active, :search, :get_by_slug    │
│  ├── :update          →   :publish, :approve, :archive           │
│  └── :destroy         →   :cancel, :soft_delete, :purge          │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

Action Lifecycle:
┌─────────────────────────────────────────────────────────────────┐
│                                                                  │
│  Input → Authorize → Validate → Change → Persist → Notify      │
│    │         │          │          │         │         │        │
│    ▼         ▼          ▼          ▼         ▼         ▼        │
│  params   policies   built-in   changes  data layer  pubsub    │
│  args     actors     custom     hooks    transaction notifiers  │
│  context  bypass     atomic                                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3. Domains: Organizing Resources

Domains group related resources and expose their actions. Think of them as bounded contexts in Domain-Driven Design.

┌─────────────────────────────────────────────────────────────────┐
│                        Application                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────┐    ┌─────────────────┐                     │
│  │  Accounts       │    │     Blog        │                     │
│  │  (Domain)       │    │   (Domain)      │                     │
│  ├─────────────────┤    ├─────────────────┤                     │
│  │ ├── User        │    │ ├── Post        │                     │
│  │ ├── Token       │    │ ├── Comment     │                     │
│  │ └── Role        │    │ └── Tag         │                     │
│  └────────┬────────┘    └────────┬────────┘                     │
│           │                      │                               │
│           └──────────┬───────────┘                               │
│                      ▼                                           │
│           ┌─────────────────┐                                    │
│           │   Shared Code   │                                    │
│           │ (Cross-domain)  │                                    │
│           └─────────────────┘                                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Domain Responsibilities:
- Expose actions to the outside world
- Define default options for queries/changesets
- Provide domain-level authorization
- Enable code organization by business capability

4. Data Layers: Persistence Abstraction

Data layers abstract the storage mechanism, allowing the same resource definition to work with different backends.

┌─────────────────────────────────────────────────────────────────┐
│                    Data Layer Abstraction                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Resource Definition (unchanged across data layers)              │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  attributes do                                             │  │
│  │    attribute :name, :string                                │  │
│  │  end                                                       │  │
│  │                                                            │  │
│  │  actions do                                                │  │
│  │    defaults [:create, :read, :update, :destroy]            │  │
│  │  end                                                       │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              │                                   │
│              ┌───────────────┼───────────────┐                   │
│              ▼               ▼               ▼                   │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐        │
│  │  AshPostgres  │  │   Ash.Ets     │  │  AshSqlite    │        │
│  ├───────────────┤  ├───────────────┤  ├───────────────┤        │
│  │ SQL queries   │  │ In-memory     │  │ File-based    │        │
│  │ Transactions  │  │ Fast tests    │  │ Embedded      │        │
│  │ Migrations    │  │ Prototyping   │  │ Single-user   │        │
│  └───────────────┘  └───────────────┘  └───────────────┘        │
│                                                                  │
│  Data Layer Capabilities:                                        │
│  ├── transact: Can wrap operations in transactions               │
│  ├── upsert: Can insert or update atomically                    │
│  ├── aggregate: Can compute aggregates in the database          │
│  ├── join: Can join resources in queries                        │
│  ├── filter: Can apply filters at the data layer                │
│  └── sort: Can sort results at the data layer                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5. Extensions: Expanding Capabilities

Extensions add functionality to resources through composition rather than inheritance.

┌─────────────────────────────────────────────────────────────────┐
│                     Extension Architecture                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  use Ash.Resource,                                               │
│    extensions: [                                                 │
│      AshPostgres.DataLayer,      ← Data persistence             │
│      AshAuthentication,           ← User authentication         │
│      AshStateMachine,             ← State transitions           │
│      AshPaperTrail,               ← Audit logging               │
│      AshArchival,                 ← Soft deletes                │
│    ]                                                             │
│                                                                  │
│  Extension Composition:                                          │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                     Base Resource                          │  │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐      │  │
│  │  │Postgres │  │  Auth   │  │ State   │  │ Audit   │      │  │
│  │  │ Layer   │  │ Actions │  │ Machine │  │ Trail   │      │  │
│  │  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘      │  │
│  │       │            │            │            │            │  │
│  │       └────────────┴────────────┴────────────┘            │  │
│  │                          │                                 │  │
│  │                  Combined Resource                         │  │
│  │            (All extensions work together)                  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

6. Query Flow: From Request to Response

Understanding how queries flow through Ash is essential for debugging and optimization.

┌─────────────────────────────────────────────────────────────────┐
│                      Query Execution Flow                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  External Request                                                │
│       │                                                          │
│       ▼                                                          │
│  ┌─────────────────┐                                            │
│  │  API Interface  │  (Phoenix, JSON:API, GraphQL)              │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Ash.Query      │  Build query with filters, sorts, loads    │
│  │  ├── filter()   │                                            │
│  │  ├── sort()     │                                            │
│  │  ├── load()     │                                            │
│  │  └── limit()    │                                            │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Authorization  │  Check policies, apply actor context       │
│  │  ├── policies   │                                            │
│  │  ├── actor      │                                            │
│  │  └── authorize? │                                            │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Data Layer     │  Execute against storage                   │
│  │  ├── translate  │  (Query → SQL)                             │
│  │  ├── execute    │                                            │
│  │  └── hydrate    │  (Rows → Structs)                          │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Post-process   │  Load relationships, calculations          │
│  │  ├── load deps  │                                            │
│  │  ├── calc values│                                            │
│  │  └── aggregates │                                            │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  Response to Client                                              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Concept Summary Table

Concept What It Is Why It Matters Key DSL
Resource Domain entity with attributes, actions, policies Single source of truth for entity behavior use Ash.Resource
Domain Group of related resources Organizes code by business capability use Ash.Domain
Action Operation that can be performed Defines explicit behavior rather than implicit CRUD actions do ... end
Attribute Data field on a resource Defines the shape of your data attribute :name, :type
Relationship Connection between resources Enables data modeling and loading belongs_to, has_many
Calculation Derived value computed on-demand Avoids storing redundant data calculate :name, :type, expr(...)
Aggregate Summary over related data Efficient counts, sums, etc. count :name, :relationship
Policy Authorization rule Declarative permissions policy action_type(...) do ... end
Data Layer Storage abstraction Enables different backends postgres do ... end
Extension Adds capabilities to resources Modular feature composition extensions: [...]

Deep Dive Reading by Concept

Official Resources

Topic Resource Why Read It
Getting Started Ash HexDocs Get Started Official tutorial with Help Desk example
Core Concepts What is Ash? Foundational understanding
Actions Actions Documentation Deep dive into action types and customization
Relationships Relationships Guide All relationship types explained
Policies Policies Documentation Authorization patterns
Calculations Calculations Guide Computed fields
Aggregates Aggregates Documentation Summary data patterns

Books

Book Chapters Focus
Ash Framework: Create Declarative Elixir Web Apps (Rebecca Le, Zach Daniel) All Comprehensive guide by the framework creator
Programming Phoenix LiveView Ch 1-4 Phoenix integration foundation
Programming Ecto Ch 3-6 Understanding the data layer
Elixir in Action Ch 5-8 OTP patterns Ash builds on

Community Resources

Resource URL Focus
Ash HQ ash-hq.org Official site with guides
Elixir Forum elixirforum.com Community Q&A
GitHub ash-project/ash Source and issues
A Gentle Primer jon.hk/elixir/ash Beginner-friendly intro

Quick Start Guide

First 48 Hours

If you’re overwhelmed by the scope of Ash, here’s what to do in your first 48 hours:

Day 1 (4 hours):

  1. Read What is Ash? (30 min)
  2. Follow the Get Started guide (2 hours)
  3. Complete Project 1: Installation and First Resource (1.5 hours)

Day 2 (4 hours):

  1. Complete Project 2: Attributes and Types (1.5 hours)
  2. Complete Project 3: Relationships (2 hours)
  3. Read Calculations and Aggregates docs (30 min)

After 48 hours, you’ll have a working Ash application with multiple resources, relationships, and understand the core concepts.


Path 1: The Pragmatic Developer (4 weeks)

For developers who want practical skills quickly:

Week 1: Foundation
├── Project 1: Installation & First Resource
├── Project 2: Attributes and Types
└── Project 3: Relationships

Week 2: Actions & Data
├── Project 4: CRUD Actions
├── Project 5: Custom Actions
└── Project 6: AshPostgres Integration

Week 3: Enhancement
├── Project 7: Calculations
├── Project 8: Aggregates
└── Project 9: Filtering and Sorting

Week 4: Integration
├── Project 10: AshPhoenix Forms
├── Project 11: Authentication
└── Project 12: Authorization Policies

Path 2: The Full-Stack Builder (8 weeks)

For developers building complete applications:

Weeks 1-4: Follow Path 1

Weeks 5-6: API Layer
├── Project 13: JSON:API
├── Project 14: GraphQL
└── Project 15: Real-time with PubSub

Weeks 7-8: Production
├── Project 16: Testing Strategies
├── Project 17: Full Application Build
└── Review and refactor earlier projects

Path 3: The Framework Deep-Diver (12 weeks)

For developers who want complete mastery:

Weeks 1-8: Follow Path 2

Weeks 9-10: Advanced Topics
├── Custom data layers
├── Extension development
└── Performance optimization

Weeks 11-12: Architecture
├── Multi-tenancy patterns
├── Event sourcing with Ash
└── Complex domain modeling

Projects


Project 1: Installation and First Resource

What You’ll Build

A Phoenix application with Ash installed and a single Ticket resource using the ETS data layer for in-memory storage.

Why This Project Matters

Understanding the installation process and basic resource structure is foundational. This project establishes the mental model you’ll use throughout Ash development: define a resource, register it with a domain, and call actions through the domain.

Core Challenges

  1. Installing Ash dependencies correctly with Igniter
  2. Understanding the resource DSL structure
  3. Connecting resources to domains
  4. Calling domain functions to perform actions

Key Concepts

  • Resources as domain entities
  • Domains as action gateways
  • The ETS data layer for prototyping
  • Ash generators and Igniter
Difficulty: Beginner Time: 2-3 hours

Prerequisites: Phoenix project creation, Mix dependencies


Real-World Outcome

After completing this project, you’ll have a Phoenix application where you can:

# In IEx
iex> alias MyApp.Support
iex> alias MyApp.Support.Ticket

# Create a ticket
iex> Ticket |> Ash.Changeset.for_create(:create, %{subject: "Help needed", description: "Cannot login"}) |> Ash.create!()
#Ticket<
  id: "a1b2c3d4-...",
  subject: "Help needed",
  description: "Cannot login",
  ...
>

# List all tickets
iex> Ash.read!(Ticket)
[#Ticket<...>]

The Core Question You’re Answering

How does Ash’s declarative resource definition translate into runnable code, and what is the relationship between resources, domains, and the functions that operate on them?


Concepts You Must Understand First

  1. What is a DSL (Domain-Specific Language)? Ash uses macros to create a specialized language for defining resources. If you can’t explain how Elixir macros work at a high level, review the Metaprogramming Elixir book.

  2. What are Mix dependencies? You must understand mix.exs deps and how Elixir packages are installed.

  3. What is ETS? Erlang Term Storage is OTP’s in-memory database. Understand that ETS data doesn’t persist across restarts.


Questions to Guide Your Design

Before writing code, answer these:

  1. What should happen when you create a resource but don’t register it with a domain?
  2. Why would you use ETS instead of PostgreSQL for initial development?
  3. What’s the difference between Ash.create! and Ash.create?
  4. How do you know what attributes a resource has?

Thinking Exercise

Before starting, trace what happens when you call:

Ticket
|> Ash.Changeset.for_create(:create, %{subject: "Test"})
|> Ash.create!()

Draw a diagram showing:

  • How the changeset is built
  • How the domain routes the action
  • How the data layer stores the record
  • What gets returned

The Interview Questions They’ll Ask

  1. “What problem does Ash Framework solve compared to raw Ecto?”
  2. “Explain the relationship between a resource and a domain.”
  3. “Why would you use the ETS data layer?”
  4. “What’s the difference between Ash.create and Ash.create!?”
  5. “How does Ash know which actions are available on a resource?”

Hints in Layers

Hint 1 - Starting Point: Use Igniter to install Ash: mix igniter.install ash. This handles configuration automatically.

Hint 2 - Next Level: Create a resource with mix ash.gen.resource MyApp.Support.Ticket. Then create a domain module that uses Ash.Domain.

Hint 3 - Technical Details: Your resource needs:

use Ash.Resource, domain: MyApp.Support, data_layer: Ash.DataLayer.Ets

attributes do
  uuid_primary_key :id
  attribute :subject, :string
end

actions do
  defaults [:create, :read]
end

Hint 4 - Verification: Test in IEx with iex -S mix. Create a ticket, then read all tickets. If both work, your setup is correct.


Books That Will Help

Topic Book Chapter
Ash fundamentals Ash Framework (Pragmatic) Ch 1-2
Phoenix project setup Programming Phoenix 1.4 Ch 1
Elixir metaprogramming Metaprogramming Elixir Ch 1-2

Common Pitfalls & Debugging

Problem Cause Fix Verification
undefined function Ash.create!/1 Ash not installed properly Run mix igniter.install ash mix deps.tree \| grep ash
No domain configured Resource missing :domain option Add domain: MyApp.Support to resource Compile without errors
No actions defined Missing actions block Add actions do defaults [:create, :read] end MyApp.Support.Ticket.__ash_actions__()
Data doesn’t persist ETS is in-memory only This is expected; switch to Postgres for persistence Restart IEx and verify data is gone

Learning Milestones

  1. Basic Understanding: You can create and read resources using IEx
  2. Working Knowledge: You understand the resource/domain relationship and can explain it
  3. Deep Understanding: You can create new resources from scratch without referring to documentation

Project 2: Attributes and Types

What You’ll Build

Extend your Ticket resource with various attribute types including strings, integers, enums, timestamps, and custom types. Add proper constraints and validations.

Why This Project Matters

Attributes define the shape of your data. Understanding Ash’s type system and constraints is crucial for data integrity. Unlike Ecto where you define schema and changeset validations separately, Ash unifies these concepts in the attribute definition.

Core Challenges

  1. Using different attribute types (string, integer, atom, map, etc.)
  2. Adding constraints (required, allow_nil, min/max length)
  3. Creating enum types for status fields
  4. Working with timestamps and default values
  5. Understanding writable vs. non-writable attributes

Key Concepts

  • Ash type system
  • Attribute constraints
  • Enum definitions
  • Default values and computed defaults
  • Private attributes
Difficulty: Beginner Time: 2-3 hours

Prerequisites: Project 1 completed


Real-World Outcome

Your Ticket resource will have rich attributes:

# Create a ticket with various attributes
iex> Ticket
...> |> Ash.Changeset.for_create(:create, %{
...>   subject: "Password reset",
...>   description: "User forgot password",
...>   priority: :high,
...>   estimated_hours: 2
...> })
...> |> Ash.create!()

#Ticket<
  id: "...",
  subject: "Password reset",
  description: "User forgot password",
  status: :open,           # Default value
  priority: :high,
  estimated_hours: 2,
  inserted_at: ~U[2024-01-15 10:30:00Z],
  updated_at: ~U[2024-01-15 10:30:00Z],
  ...
>

# Validation errors for missing required fields
iex> Ticket |> Ash.Changeset.for_create(:create, %{}) |> Ash.create()
{:error, %Ash.Error.Invalid{
  errors: [
    %Ash.Error.Changes.Required{field: :subject, ...}
  ]
}}

The Core Question You’re Answering

How does Ash’s attribute system differ from Ecto schemas, and how do constraints provide validation at the data definition level rather than in separate changeset functions?


Concepts You Must Understand First

  1. Elixir type specs: How Elixir types work (atoms, strings, integers)
  2. Ecto types: Ash builds on Ecto’s type system
  3. Changeset validations: Understand how Ecto validates before storage

Questions to Guide Your Design

  1. Should status be a string or an enum? What are the tradeoffs?
  2. When should an attribute have allow_nil?: false?
  3. What’s the difference between default and create_default?
  4. Why might you make an attribute private?: true?

Thinking Exercise

Design the attributes for a User resource that includes:

  • Email (required, unique, must be valid format)
  • Name (required, min 2 characters)
  • Role (enum: admin, member, guest)
  • Bio (optional, max 500 characters)
  • Verified status (boolean, default false)
  • Timestamps

Write out the attribute definitions before implementing.


The Interview Questions They’ll Ask

  1. “How do you define an enum type in Ash?”
  2. “What’s the difference between allow_nil? and required??”
  3. “How do you set default values that are computed at runtime?”
  4. “What constraints are available for string attributes?”
  5. “How do you prevent an attribute from being set by external input?”

Hints in Layers

Hint 1 - Starting Point: Define an enum with defmodule MyApp.Support.Ticket.Status do use Ash.Type.Enum, values: [:open, :in_progress, :closed] end

Hint 2 - Next Level: Use constraints for validation: attribute :subject, :string, allow_nil?: false, constraints: [min_length: 5, max_length: 200]

Hint 3 - Technical Details:

attributes do
  uuid_primary_key :id

  attribute :subject, :string do
    allow_nil? false
    constraints min_length: 5, max_length: 200
  end

  attribute :status, MyApp.Support.Ticket.Status do
    default :open
    allow_nil? false
  end

  create_timestamp :inserted_at
  update_timestamp :updated_at
end

Hint 4 - Verification: Try creating tickets with invalid data and verify you get proper error messages.


Common Pitfalls & Debugging

Problem Cause Fix Verification
Enum not found Module not compiled Ensure enum module is defined before resource Code.ensure_loaded(MyApp.Support.Ticket.Status)
Constraint not validated Wrong constraint name Check docs for exact constraint names Read error messages carefully
Default not applied Using default instead of create_default Use create_default for runtime computation Create record and check value

Project 3: Relationships

What You’ll Build

Create a complete support system with Users, Tickets, and Comments. Implement belongs_to, has_many, and many_to_many relationships.

Why This Project Matters

Relationships are the backbone of data modeling. Ash relationships work differently than Ecto - they’re first-class citizens that integrate with actions, policies, and loading. Understanding relationship loading, managing related data through actions, and querying across relationships is essential.

Core Challenges

  1. Defining belongs_to, has_many, and many_to_many relationships
  2. Managing foreign keys and source/destination attributes
  3. Loading relationships in queries
  4. Understanding relationship cardinality
  5. Creating records with related data

Key Concepts

  • Relationship types and when to use each
  • Source and destination attributes
  • Relationship loading with Ash.load
  • Nested data management
Difficulty: Beginner-Intermediate Time: 3-4 hours

Prerequisites: Projects 1-2 completed


Real-World Outcome

# Create a user
iex> user = User |> Ash.Changeset.for_create(:create, %{email: "dev@example.com"}) |> Ash.create!()

# Create a ticket belonging to the user
iex> ticket = Ticket
...> |> Ash.Changeset.for_create(:create, %{
...>   subject: "Help needed",
...>   reporter_id: user.id
...> })
...> |> Ash.create!()

# Load the relationship
iex> ticket |> Ash.load!(:reporter)
#Ticket<
  reporter: #User<email: "dev@example.com", ...>,
  ...
>

# Query tickets with related data
iex> Ticket |> Ash.Query.load(:reporter) |> Ash.read!()
[#Ticket<reporter: #User<...>, ...>]

# Access user's tickets
iex> user |> Ash.load!(:tickets)
#User<
  tickets: [#Ticket<...>],
  ...
>

The Core Question You’re Answering

How do Ash relationships differ from Ecto associations, and how does the loading mechanism work to fetch related data efficiently?


Concepts You Must Understand First

  1. Database foreign keys: How relationships are stored at the database level
  2. Ecto associations: Basic understanding of has_many/belongs_to in Ecto
  3. N+1 query problem: Why relationship loading strategy matters

Questions to Guide Your Design

  1. Should the foreign key be on Ticket or User for a “reporter” relationship?
  2. How do you ensure a ticket always has a reporter?
  3. When would you use has_one vs has_many?
  4. How do you load nested relationships (ticket -> reporter -> organization)?

Thinking Exercise

Draw an ER diagram for this system:

  • Users have many Tickets (as reporter)
  • Tickets have many Comments
  • Comments belong to a User (author) and a Ticket
  • Users can be assigned to Tickets (many-to-many through assignments)

Label all foreign keys and cardinality.


The Interview Questions They’ll Ask

  1. “Explain the difference between belongs_to and has_many in Ash.”
  2. “How do you prevent N+1 queries when loading relationships?”
  3. “What happens if you try to delete a user who has tickets?”
  4. “How do you implement a many-to-many relationship?”
  5. “Can you have multiple relationships to the same resource?”

Hints in Layers

Hint 1 - Starting Point: Start with two resources (User, Ticket) and a single belongs_to relationship from Ticket to User.

Hint 2 - Next Level: Define both sides of the relationship:

  • Ticket: belongs_to :reporter, User
  • User: has_many :tickets, Ticket, destination_attribute: :reporter_id

Hint 3 - Technical Details:

# In Ticket
relationships do
  belongs_to :reporter, MyApp.Accounts.User do
    allow_nil? false
    attribute_type :uuid
  end
end

# In User
relationships do
  has_many :tickets, MyApp.Support.Ticket do
    destination_attribute :reporter_id
  end
end

Hint 4 - Verification: Create a ticket with a reporter, then load the reporter. Create a user, then load their tickets.


Common Pitfalls & Debugging

Problem Cause Fix Verification
relationship not found Typo in relationship name Check exact name in load/1 Resource.__ash_relationships__()
Missing foreign key belongs_to doesn’t auto-create attribute It does in Ash 3.x, check attribute exists Resource.__ash_attributes__()
Circular dependency Resources reference each other Use string reference: belongs_to :reporter, "MyApp.Accounts.User" Compile without errors

Project 4: CRUD Actions

What You’ll Build

Implement comprehensive CRUD (Create, Read, Update, Destroy) actions for your support system with validation, error handling, and action arguments.

Why This Project Matters

Actions are how the outside world interacts with your resources. Unlike traditional frameworks where you write controller actions, Ash actions are defined on the resource itself. This project teaches you how actions work, how to customize them, and how to use action arguments effectively.

Core Challenges

  1. Understanding default CRUD actions
  2. Customizing action accepts/rejects
  3. Using action arguments vs attributes
  4. Implementing action changes (before/after hooks)
  5. Understanding atomic vs non-atomic operations

Key Concepts

  • Action types (create, read, update, destroy)
  • Primary actions
  • Action arguments
  • Accept/reject options
  • Manual actions
Difficulty: Intermediate Time: 3-4 hours

Prerequisites: Projects 1-3 completed


Real-World Outcome

# Create with specific accepted attributes
iex> Ticket
...> |> Ash.Changeset.for_create(:open_ticket, %{
...>   subject: "Bug report",
...>   description: "App crashes on startup",
...>   reporter_id: user.id
...> })
...> |> Ash.create!()

# Update only allowed fields
iex> ticket
...> |> Ash.Changeset.for_update(:assign, %{assignee_id: agent.id})
...> |> Ash.update!()

# Read with filters
iex> Ticket
...> |> Ash.Query.filter(status == :open)
...> |> Ash.read!()

# Destroy with soft-delete behavior
iex> ticket |> Ash.Changeset.for_destroy(:close) |> Ash.destroy!()

The Core Question You’re Answering

How do Ash actions provide a layer of abstraction over raw data operations, and why is it better to define many specific actions rather than few generic ones?


Concepts You Must Understand First

  1. Changesets: How Ash builds changesets for actions
  2. Action semantics: What each action type means
  3. Elixir pattern matching: For action arguments

Questions to Guide Your Design

  1. Should you have one :update action or many specific ones (:assign, :close, :escalate)?
  2. When should you use accept vs reject for attributes?
  3. What’s the difference between an action argument and an accepted attribute?
  4. How do you ensure only certain fields can be changed through a specific action?

Thinking Exercise

Design the actions for a Ticket resource:

  • Create actions: :open (new tickets), :create_from_email (automated)
  • Read actions: :list_open, :list_by_assignee, :search
  • Update actions: :assign, :unassign, :change_priority, :add_note, :close
  • Destroy actions: :delete, :archive

For each, list what attributes/arguments it should accept.


The Interview Questions They’ll Ask

  1. “Why does Ash encourage many specific actions instead of generic CRUD?”
  2. “What’s the difference between accept and argument in an action?”
  3. “How do you make an action the primary action for its type?”
  4. “Explain the action lifecycle: what happens before and after the data layer?”
  5. “How do you add side effects to an action?”

Hints in Layers

Hint 1 - Starting Point: Start with defaults [:create, :read, :update, :destroy], then customize one at a time.

Hint 2 - Next Level: Create a specific action:

update :assign do
  accept []
  argument :assignee_id, :uuid, allow_nil?: false
  change set_attribute(:assignee_id, arg(:assignee_id))
end

Hint 3 - Technical Details: Use changes for action logic:

create :open_ticket do
  accept [:subject, :description]
  change set_attribute(:status, :open)
  change relate_actor(:reporter)
end

Hint 4 - Verification: Try calling each action and verify only the intended attributes can be changed.


Common Pitfalls & Debugging

Problem Cause Fix Verification
Attribute not accepted Not in accept list Add to accept [:field] or use argument Check error message
Action not found Typo or not defined Check action name exactly Resource.__ash_actions__()
Changes not applied Missing change call Add change set_attribute(...) Read after write

Project 5: Custom Actions and Changes

What You’ll Build

Create custom actions with complex business logic including state transitions, side effects, and multi-step workflows.

Why This Project Matters

Real applications have complex business logic that goes beyond simple CRUD. This project teaches you to encapsulate domain logic within actions using changes, validations, and manual implementations. You’ll learn when to use built-in changes vs custom change modules.

Core Challenges

  1. Writing custom change modules
  2. Implementing state machine transitions
  3. Adding validations that span multiple fields
  4. Orchestrating multi-step operations
  5. Using manual actions for complex logic

Key Concepts

  • Custom Ash.Resource.Change modules
  • Validations
  • Manual actions
  • State transitions
  • Action composition
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 4 completed


Real-World Outcome

# State transition with validation
iex> ticket
...> |> Ash.Changeset.for_update(:resolve, %{resolution_notes: "Fixed the bug"})
...> |> Ash.update!()

# Returns error if transition is invalid
iex> closed_ticket |> Ash.Changeset.for_update(:resolve, %{}) |> Ash.update()
{:error, %Ash.Error.Invalid{
  errors: [%MyApp.Support.Ticket.InvalidTransition{
    from: :closed,
    to: :resolved
  }]
}}

# Manual action with complex logic
iex> Support.escalate_ticket(ticket, level: 2, notify: [:manager, :oncall])
{:ok, %Ticket{priority: :urgent, escalation_level: 2}}

The Core Question You’re Answering

How do you encapsulate complex business logic within Ash’s declarative model while maintaining testability and composability?


Concepts You Must Understand First

  1. Elixir behaviours: How to implement callback modules
  2. State machines: Valid state transitions
  3. Ecto multi: Transaction patterns (Ash handles this, but understand why)

Questions to Guide Your Design

  1. When should logic go in a change module vs inline in the action?
  2. How do you validate that a state transition is allowed?
  3. What’s the difference between a validation and a change?
  4. When should you use a manual action vs composition of changes?

Thinking Exercise

Design a ticket escalation workflow:

  1. Ticket can be escalated to levels 1-3
  2. Each escalation sends notifications to different people
  3. Escalation changes priority automatically
  4. Cannot escalate closed tickets
  5. Level 3 escalation creates a linked incident record

Write pseudocode for this workflow as an Ash action with changes.


The Interview Questions They’ll Ask

  1. “How do you implement a custom change in Ash?”
  2. “What’s the difference between change and validate in an action?”
  3. “How do you share logic between multiple actions?”
  4. “When would you use a manual action?”
  5. “How do you handle rollback logic in Ash?”

Hints in Layers

Hint 1 - Starting Point: Create a change module:

defmodule MyApp.Support.Changes.SetPriority do
  use Ash.Resource.Change

  def change(changeset, _opts, _context) do
    Ash.Changeset.force_change_attribute(changeset, :priority, :high)
  end
end

Hint 2 - Next Level: Add validations for state transitions:

validate fn changeset, _context ->
  current = Ash.Changeset.get_data(changeset, :status)
  new = Ash.Changeset.get_attribute(changeset, :status)

  if valid_transition?(current, new) do
    :ok
  else
    {:error, field: :status, message: "Invalid transition"}
  end
end

Hint 3 - Technical Details: For manual actions:

action :escalate, :struct do
  constraints instance_of: __MODULE__
  argument :level, :integer
  manual MyApp.Support.Actions.Escalate
end

Hint 4 - Verification: Test invalid transitions and verify proper error messages are returned.


Project 6: AshPostgres Data Layer

What You’ll Build

Migrate your support system from ETS to PostgreSQL using AshPostgres. Implement migrations, indexes, and database-specific features.

Why This Project Matters

Production applications need persistent storage. AshPostgres provides a bridge between Ash’s declarative model and PostgreSQL’s powerful features. Understanding how Ash generates migrations, handles schema changes, and optimizes queries is essential for real-world applications.

Core Challenges

  1. Installing and configuring AshPostgres
  2. Generating and running migrations
  3. Adding indexes for performance
  4. Understanding how Ash translates to SQL
  5. Using PostgreSQL-specific features

Key Concepts

  • AshPostgres.DataLayer
  • Migrations with mix ash.codegen
  • Indexes and constraints
  • References and foreign keys
  • Custom SQL types
Difficulty: Intermediate Time: 3-4 hours

Prerequisites: Projects 1-5, PostgreSQL installed


Real-World Outcome

# Generate migrations
$ mix ash.codegen initial_migration

# Run migrations
$ mix ash_postgres.migrate

# Check generated SQL
$ mix ash_postgres.migrate --dry-run
# Data persists across restarts
iex> Ticket |> Ash.Changeset.for_create(:create, %{subject: "Test"}) |> Ash.create!()
iex> # Restart IEx
iex> Ash.read!(Ticket)
[#Ticket<subject: "Test", ...>]

# Complex queries translated to efficient SQL
iex> Ticket
...> |> Ash.Query.filter(status == :open and inserted_at > ago(7, :day))
...> |> Ash.Query.sort(priority: :desc)
...> |> Ash.Query.load(:reporter)
...> |> Ash.read!()

The Core Question You’re Answering

How does Ash’s declarative resource definition translate to PostgreSQL schemas and queries, and how do you optimize database performance within the Ash model?


Concepts You Must Understand First

  1. PostgreSQL basics: Tables, columns, constraints, indexes
  2. Ecto migrations: How schema changes are managed
  3. SQL queries: Understanding SELECT, JOIN, WHERE

Questions to Guide Your Design

  1. What indexes should you add for your most common queries?
  2. How do you handle schema changes after initial deployment?
  3. When should you use database constraints vs application validations?
  4. How do you debug slow queries?

Thinking Exercise

Review your Ticket and User resources. List:

  1. All the tables that will be created
  2. All foreign key relationships
  3. Indexes you need for these queries:
    • Find all open tickets
    • Find tickets by reporter email
    • Find recent tickets (last 7 days)
    • Full-text search on ticket subject

The Interview Questions They’ll Ask

  1. “How does AshPostgres generate migrations?”
  2. “What’s the difference between mix ash.codegen and mix ecto.gen.migration?”
  3. “How do you add custom indexes in Ash?”
  4. “How do you handle database-specific column types?”
  5. “How does Ash handle relationship queries at the database level?”

Hints in Layers

Hint 1 - Starting Point: Install with mix igniter.install ash_postgres. Update your repo to use AshPostgres.Repo.

Hint 2 - Next Level: Add PostgreSQL data layer to resources:

use Ash.Resource,
  domain: MyApp.Support,
  data_layer: AshPostgres.DataLayer

postgres do
  table "tickets"
  repo MyApp.Repo
end

Hint 3 - Technical Details: Add indexes:

postgres do
  table "tickets"
  repo MyApp.Repo

  custom_indexes do
    index [:status]
    index [:inserted_at]
    index [:reporter_id, :status]
  end
end

Hint 4 - Verification: Run migrations, create data, restart IEx, and verify data persists.


Common Pitfalls & Debugging

Problem Cause Fix Verification
Migration fails Missing references Ensure related resources are migrated first Check migration order
Slow queries Missing indexes Add indexes for filter/sort columns EXPLAIN ANALYZE in psql
Type mismatch Ash type vs Postgres type Use migration_types for custom mappings Check migration file

Project 7: Calculations

What You’ll Build

Add calculated fields to your resources including expression-based calculations, function calculations, and calculations with arguments.

Why This Project Matters

Calculations derive values from existing data without storing them. They’re essential for display formatting, computed fields, and business metrics. Ash calculations are powerful because they can be filtered and sorted on, unlike simple virtual fields.

Core Challenges

  1. Writing expression-based calculations
  2. Creating function-based calculations
  3. Calculations with arguments
  4. Loading calculations efficiently
  5. Filtering and sorting on calculations

Key Concepts

  • Ash expressions
  • Calculation types
  • Arguments in calculations
  • Calculation context
  • SQL expression translation
Difficulty: Intermediate Time: 3-4 hours

Prerequisites: Project 6 completed


Real-World Outcome

# Expression calculation
iex> User
...> |> Ash.Query.load(:full_name)
...> |> Ash.read!()
[#User<full_name: "John Doe", ...>]

# Calculation with argument
iex> Ticket
...> |> Ash.Query.load(age: [unit: :hour])
...> |> Ash.read!()
[#Ticket<age: 48, ...>]

# Filter on calculation
iex> Ticket
...> |> Ash.Query.filter(age(unit: :day) > 7)
...> |> Ash.read!()
[#Ticket<...>]  # Only tickets older than 7 days

# Sort on calculation
iex> User
...> |> Ash.Query.sort(:full_name)
...> |> Ash.read!()
[#User<full_name: "Alice Smith">, #User<full_name: "Bob Jones">]

The Core Question You’re Answering

How do Ash calculations provide computed values that can be loaded on-demand, filtered on, and sorted by, all while maintaining database efficiency?


Concepts You Must Understand First

  1. SQL expressions: How calculations translate to SQL
  2. Virtual fields: The concept of non-persisted values
  3. Ash expressions: The expr/1 macro and syntax

Questions to Guide Your Design

  1. Should this value be a calculation or a stored attribute?
  2. Can this calculation be computed in the database?
  3. What arguments might this calculation need?
  4. How will this calculation perform at scale?

Thinking Exercise

Design calculations for these requirements:

  1. User full name (first + last)
  2. Ticket age in various units (days, hours, minutes)
  3. User’s open ticket count
  4. Ticket urgency score (based on priority, age, and status)

For each, determine if it should be an expression or function calculation.


The Interview Questions They’ll Ask

  1. “What’s the difference between an expression and function calculation?”
  2. “How do you make a calculation filterable?”
  3. “When would you use a calculation vs an aggregate?”
  4. “How do calculations affect query performance?”
  5. “Can calculations reference relationships?”

Hints in Layers

Hint 1 - Starting Point: Simple expression calculation:

calculations do
  calculate :full_name, :string, expr(first_name <> " " <> last_name)
end

Hint 2 - Next Level: Calculation with arguments:

calculate :age, :integer do
  argument :unit, :atom, default: :day, constraints: [one_of: [:day, :hour, :minute]]

  calculation expr(
    fragment("EXTRACT(EPOCH FROM (NOW() - ?))", inserted_at) /
    case ^arg(:unit) do
      :day -> 86400
      :hour -> 3600
      :minute -> 60
    end
  )
end

Hint 3 - Technical Details: For complex logic, use a module:

defmodule MyApp.Calculations.UrgencyScore do
  use Ash.Resource.Calculation

  def calculate(records, opts, context) do
    Enum.map(records, fn record ->
      # Complex calculation logic
      base_score = priority_score(record.priority)
      age_factor = age_factor(record.inserted_at)
      base_score * age_factor
    end)
  end
end

Hint 4 - Verification: Load calculations, then filter on them, then sort on them.


Project 8: Aggregates

What You’ll Build

Add aggregate fields for counting, summing, and analyzing related data. Implement efficient queries that leverage database aggregations.

Why This Project Matters

Aggregates summarize related data (count of tickets, sum of hours, average rating). Unlike calculations, aggregates specifically work over relationships and are optimized for database-level computation. Understanding when to use aggregates vs calculations is key to performant applications.

Core Challenges

  1. Defining count, sum, first, list aggregates
  2. Filtering aggregates
  3. Using aggregates in filters and sorts
  4. Combining aggregates with calculations
  5. Understanding aggregate performance

Key Concepts

  • Aggregate types (count, sum, first, list, exists, max, min)
  • Filtered aggregates
  • Aggregates on nested relationships
  • Loading and filtering on aggregates
Difficulty: Intermediate Time: 3-4 hours

Prerequisites: Project 7 completed


Real-World Outcome

# Count aggregate
iex> User
...> |> Ash.Query.load(:ticket_count)
...> |> Ash.read!()
[#User<ticket_count: 5, ...>]

# Filtered aggregate
iex> User
...> |> Ash.Query.load(:open_ticket_count)
...> |> Ash.read!()
[#User<open_ticket_count: 3, ...>]

# Sort by aggregate
iex> User
...> |> Ash.Query.sort(ticket_count: :desc)
...> |> Ash.Query.limit(10)
...> |> Ash.read!()
# Top 10 users by ticket count

# Filter by aggregate
iex> User
...> |> Ash.Query.filter(ticket_count > 10)
...> |> Ash.read!()
# Users with more than 10 tickets

# First aggregate (latest ticket)
iex> User
...> |> Ash.Query.load(:latest_ticket_subject)
...> |> Ash.read!()
[#User<latest_ticket_subject: "Password reset", ...>]

The Core Question You’re Answering

How do aggregates provide efficient summary data over relationships, and when should you choose aggregates over manual queries or calculations?


Concepts You Must Understand First

  1. SQL aggregations: GROUP BY, COUNT, SUM, MAX, MIN
  2. Subqueries: How aggregates translate to SQL
  3. N+1 problem: Why aggregates are loaded efficiently

Questions to Guide Your Design

  1. What summary data would be useful for your resources?
  2. Should you filter the aggregate or filter the main query?
  3. What’s the performance difference between aggregates in SQL vs Elixir?
  4. When would you use :first vs :list aggregate?

Thinking Exercise

Design aggregates for User resource:

  1. Total number of tickets
  2. Number of open tickets
  3. Number of closed tickets this month
  4. Latest ticket subject
  5. Average ticket resolution time (if you have resolved_at)

Write the aggregate definitions before implementing.


The Interview Questions They’ll Ask

  1. “What aggregate types does Ash support?”
  2. “How do you filter an aggregate?”
  3. “What’s the difference between an aggregate and a calculation?”
  4. “How do aggregates perform at scale?”
  5. “Can you have aggregates on nested relationships?”

Hints in Layers

Hint 1 - Starting Point: Basic count aggregate:

aggregates do
  count :ticket_count, :tickets
end

Hint 2 - Next Level: Filtered aggregate:

aggregates do
  count :open_ticket_count, :tickets do
    filter expr(status == :open)
  end
end

Hint 3 - Technical Details: First aggregate for latest item:

aggregates do
  first :latest_ticket_subject, :tickets, :subject do
    sort inserted_at: :desc
  end

  sum :total_estimated_hours, :tickets, :estimated_hours do
    filter expr(status != :closed)
  end
end

Hint 4 - Verification: Load aggregates and verify values match manual counts. Check SQL with Ash.Query.to_sql/1.


Project 9: Filtering and Sorting

What You’ll Build

Implement comprehensive query capabilities with complex filters, sorting, pagination, and search functionality.

Why This Project Matters

Real applications need sophisticated query capabilities. Ash provides a powerful filtering DSL that translates to efficient SQL. Understanding how to build queries, combine filters, and paginate results is essential for any data-driven application.

Core Challenges

  1. Building complex filter expressions
  2. Combining multiple sort criteria
  3. Implementing keyset and offset pagination
  4. Full-text search
  5. Dynamic filters from user input

Key Concepts

  • Ash.Query.filter expressions
  • Sort specifications
  • Keyset vs offset pagination
  • Filter predicates (==, in, contains, etc.)
  • Safe user input handling
Difficulty: Intermediate Time: 3-4 hours

Prerequisites: Project 8 completed


Real-World Outcome

# Complex filter
iex> Ticket
...> |> Ash.Query.filter(
...>   status in [:open, :in_progress] and
...>   priority == :high and
...>   inserted_at > ago(7, :day)
...> )
...> |> Ash.read!()

# Multiple sort criteria
iex> Ticket
...> |> Ash.Query.sort(priority: :desc, inserted_at: :asc)
...> |> Ash.read!()

# Keyset pagination
iex> page = Ticket
...> |> Ash.Query.sort(:inserted_at)
...> |> Ash.read!(page: [limit: 20])

iex> next_page = Ash.page!(page, :next)

# Search
iex> Ticket
...> |> Ash.Query.filter(contains(subject, "password"))
...> |> Ash.read!()

# Filter across relationships
iex> Ticket
...> |> Ash.Query.filter(reporter.email == "user@example.com")
...> |> Ash.read!()

The Core Question You’re Answering

How does Ash’s query DSL provide type-safe, efficient filtering that translates to optimized SQL while remaining composable and easy to use?


Concepts You Must Understand First

  1. Boolean algebra: AND, OR, NOT combinations
  2. SQL WHERE clauses: How filters translate
  3. Pagination strategies: Offset vs cursor/keyset tradeoffs

Questions to Guide Your Design

  1. How do you safely accept filter parameters from users?
  2. When should you use keyset vs offset pagination?
  3. How do you filter on related resources?
  4. What indexes do you need for your common filters?

Thinking Exercise

Design filters for a ticket search API:

  • Filter by status (multiple values)
  • Filter by priority
  • Filter by date range
  • Filter by reporter email (across relationship)
  • Full-text search on subject and description
  • Sort by any combination of fields

Write the Ash.Query code for each.


The Interview Questions They’ll Ask

  1. “What filter predicates does Ash support?”
  2. “How do you prevent SQL injection in dynamic filters?”
  3. “Explain keyset vs offset pagination tradeoffs.”
  4. “How do you filter across relationships?”
  5. “How do you implement full-text search in Ash?”

Hints in Layers

Hint 1 - Starting Point: Basic filtering:

Ticket
|> Ash.Query.filter(status == :open)
|> Ash.Query.filter(priority == :high)
|> Ash.read!()

Hint 2 - Next Level: Dynamic filters from user input:

def search(params) do
  Ticket
  |> Ash.Query.for_read(:list, params)
  |> Ash.read!()
end

# In resource
read :list do
  argument :status, {:array, :atom}
  argument :priority, :atom

  filter expr(
    (is_nil(^arg(:status)) or status in ^arg(:status)) and
    (is_nil(^arg(:priority)) or priority == ^arg(:priority))
  )
end

Hint 3 - Technical Details: Keyset pagination:

read :list do
  pagination keyset?: true, required?: false, default_limit: 20, max_page_size: 100
end

Hint 4 - Verification: Test with various filter combinations and verify SQL efficiency.


Project 10: AshPhoenix Forms Integration

What You’ll Build

Integrate Ash with Phoenix LiveView forms using AshPhoenix. Build interactive forms with validation, error handling, and nested data.

Why This Project Matters

AshPhoenix bridges Ash resources with Phoenix’s form handling. It provides form structs that work seamlessly with LiveView components while leveraging Ash’s validation and action system. This is how most Ash web applications handle user input.

Core Challenges

  1. Creating forms from Ash resources
  2. Handling form validation in LiveView
  3. Nested forms for relationships
  4. Form state management
  5. Error display and recovery

Key Concepts

  • AshPhoenix.Form
  • LiveView form handling
  • Nested forms
  • Form to action flow
  • Error handling
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 9, Phoenix LiveView basics


Real-World Outcome

# In LiveView
def mount(_params, _session, socket) do
  form = AshPhoenix.Form.for_create(Ticket, :create, as: "ticket")
  {:ok, assign(socket, form: to_form(form))}
end

def handle_event("validate", %{"ticket" => params}, socket) do
  form = AshPhoenix.Form.validate(socket.assigns.form.source, params)
  {:noreply, assign(socket, form: to_form(form))}
end

def handle_event("save", %{"ticket" => params}, socket) do
  case AshPhoenix.Form.submit(socket.assigns.form.source, params: params) do
    {:ok, ticket} ->
      {:noreply,
       socket
       |> put_flash(:info, "Ticket created!")
       |> push_navigate(to: ~p"/tickets/#{ticket.id}")}

    {:error, form} ->
      {:noreply, assign(socket, form: to_form(form))}
  end
end
<.form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:subject]} label="Subject" />
  <.input field={@form[:description]} type="textarea" label="Description" />
  <.input field={@form[:priority]} type="select" options={priority_options()} />

  <:actions>
    <.button>Create Ticket</.button>
  </:actions>
</.form>

The Core Question You’re Answering

How does AshPhoenix.Form bridge the gap between Ash’s action system and Phoenix’s form handling, enabling real-time validation and seamless data submission?


Concepts You Must Understand First

  1. Phoenix LiveView forms: to_form/1, form events
  2. Changesets: Ash uses changesets internally
  3. LiveView assigns: State management in LiveView

Questions to Guide Your Design

  1. When should validation run (on change vs on submit)?
  2. How do you display errors for nested forms?
  3. How do you handle forms for existing records (updates)?
  4. What happens when the actor context is needed?

Thinking Exercise

Design forms for:

  1. Create ticket (new form)
  2. Edit ticket (update form)
  3. Create ticket with comments (nested form)
  4. Assign ticket to user (select from relationship)

Sketch the LiveView structure for each.


The Interview Questions They’ll Ask

  1. “How does AshPhoenix.Form differ from Ecto changesets in forms?”
  2. “How do you handle nested forms with AshPhoenix?”
  3. “How do you pass actor context to forms?”
  4. “What’s the validation flow for AshPhoenix forms?”
  5. “How do you handle form errors in LiveView?”

Hints in Layers

Hint 1 - Starting Point: Install AshPhoenix: mix igniter.install ash_phoenix. Create a basic form in LiveView.

Hint 2 - Next Level: Pass actor to forms:

form = AshPhoenix.Form.for_create(Ticket, :create,
  as: "ticket",
  actor: socket.assigns.current_user
)

Hint 3 - Technical Details: Nested forms for relationships:

form = AshPhoenix.Form.for_create(Ticket, :create_with_comment,
  forms: [
    comment: [
      resource: Comment,
      create_action: :create
    ]
  ]
)

Hint 4 - Verification: Test full flow: render form, enter invalid data, see errors, fix errors, submit successfully.


Project 11: Authentication with AshAuthentication

What You’ll Build

Add user authentication to your application using AshAuthentication with password-based login, registration, and session management.

Why This Project Matters

Authentication is required for almost every web application. AshAuthentication integrates deeply with Ash’s resource model, providing authentication as a declarative extension rather than a separate system. This means your auth logic follows the same patterns as the rest of your application.

Core Challenges

  1. Installing and configuring AshAuthentication
  2. Adding authentication to User resource
  3. Implementing registration and login
  4. Session management with tokens
  5. Phoenix integration with plugs

Key Concepts

  • AshAuthentication extension
  • Password strategy
  • Token generation and validation
  • Phoenix authentication plugs
  • Session vs token auth
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 10 completed


Real-World Outcome

# Register a user
iex> User
...> |> Ash.Changeset.for_create(:register_with_password, %{
...>   email: "user@example.com",
...>   password: "securepassword123",
...>   password_confirmation: "securepassword123"
...> })
...> |> Ash.create!()

# Sign in
iex> strategy = AshAuthentication.Info.strategy!(User, :password)
iex> AshAuthentication.Strategy.action(strategy, :sign_in, %{
...>   email: "user@example.com",
...>   password: "securepassword123"
...> })
{:ok, user, token}

# In Phoenix
# GET /sign-in renders login form
# POST /sign-in attempts authentication
# GET /register renders registration form
# POST /register creates account
# DELETE /sign-out logs out

The Core Question You’re Answering

How does AshAuthentication extend the resource model to provide declarative authentication, and how does this integrate with Phoenix’s request lifecycle?


Concepts You Must Understand First

  1. Password hashing: Why we hash, common algorithms
  2. Session management: Cookies, tokens, expiration
  3. Phoenix plugs: How authentication middleware works

Questions to Guide Your Design

  1. Should you use session-based or token-based authentication?
  2. How do you handle “remember me” functionality?
  3. What password requirements should you enforce?
  4. How do you protect routes that require authentication?

Thinking Exercise

Design your authentication flow:

  1. Registration with email confirmation (optional)
  2. Login with password
  3. Password reset flow
  4. Session timeout behavior
  5. Multi-device session handling

Sketch the database fields and routes needed.


The Interview Questions They’ll Ask

  1. “How does AshAuthentication integrate with Ash resources?”
  2. “What authentication strategies does AshAuthentication support?”
  3. “How do you protect routes in Phoenix with AshAuthentication?”
  4. “How are passwords stored securely?”
  5. “How do you implement ‘remember me’ functionality?”

Hints in Layers

Hint 1 - Starting Point: Install: mix igniter.install ash_authentication ash_authentication_phoenix

Hint 2 - Next Level: Add to User resource:

use Ash.Resource,
  extensions: [AshAuthentication]

authentication do
  strategies do
    password :password do
      identity_field :email
      hashed_password_field :hashed_password
    end
  end

  tokens do
    enabled? true
    token_resource MyApp.Accounts.Token
    signing_secret fn _, _ ->
      Application.get_env(:my_app, :token_signing_secret)
    end
  end
end

Hint 3 - Technical Details: Add Phoenix routes:

# In router.ex
use AshAuthentication.Phoenix.Router

ash_authentication_routes(MyApp.Accounts.User)

Hint 4 - Verification: Register a user, sign in, access protected route, sign out, verify cannot access protected route.


Project 12: Authorization Policies

What You’ll Build

Implement comprehensive authorization using Ash policies. Define who can do what with fine-grained access control based on roles, ownership, and resource state.

Why This Project Matters

Authorization determines who can perform which actions. Ash policies are declarative and composable, co-located with resource definitions. This is fundamentally different from scattered authorization checks in controllers. Understanding policies is critical for secure applications.

Core Challenges

  1. Writing policy conditions
  2. Role-based access control
  3. Ownership-based permissions
  4. Field-level authorization
  5. Debugging authorization failures

Key Concepts

  • Ash.Policy.Authorizer
  • Policy conditions and checks
  • Actors and context
  • Bypass policies
  • Field policies
Difficulty: Intermediate-Advanced Time: 5-6 hours

Prerequisites: Project 11 completed


Real-World Outcome

# Policy passes - user can read their own tickets
iex> Ash.read!(Ticket, actor: user)
[#Ticket<reporter_id: user.id, ...>]

# Policy fails - user cannot read others' tickets (filtered out)
iex> Ash.read!(Ticket, actor: user)
[]  # Other tickets filtered

# Policy passes - admin can read all tickets
iex> Ash.read!(Ticket, actor: admin)
[#Ticket<...>, #Ticket<...>, ...]

# Policy fails - unauthorized action
iex> ticket
...> |> Ash.Changeset.for_update(:assign, %{}, actor: regular_user)
...> |> Ash.update()
{:error, %Ash.Error.Forbidden{}}

# Policy passes - manager can assign
iex> ticket
...> |> Ash.Changeset.for_update(:assign, %{assignee_id: agent.id}, actor: manager)
...> |> Ash.update!()

The Core Question You’re Answering

How do Ash policies provide declarative, composable authorization that’s co-located with resources and automatically applied across all access paths?


Concepts You Must Understand First

  1. Authorization vs authentication: Auth = who you are, Authz = what you can do
  2. RBAC: Role-based access control patterns
  3. ABAC: Attribute-based access control

Questions to Guide Your Design

  1. What roles exist in your system?
  2. Which resources have ownership concepts?
  3. Should read actions filter or reject?
  4. What’s the default policy (allow or deny)?

Thinking Exercise

Design policies for a ticket system:

  • Users can read tickets they reported
  • Users can read tickets assigned to them
  • Managers can read all tickets in their department
  • Admins can read all tickets
  • Only managers can assign tickets
  • Users can only update their own ticket descriptions
  • Anyone can add comments, but only to tickets they can read

Write the policy definitions before implementing.


The Interview Questions They’ll Ask

  1. “How do Ash policies differ from controller-based authorization?”
  2. “What’s the difference between authorize_if and forbid_if?”
  3. “How do you implement row-level security?”
  4. “How do you debug authorization failures?”
  5. “How do policies interact with read actions?”

Hints in Layers

Hint 1 - Starting Point: Add authorizer to resource:

use Ash.Resource,
  authorizers: [Ash.Policy.Authorizer]

policies do
  policy action_type(:read) do
    authorize_if actor_attribute_equals(:role, :admin)
  end
end

Hint 2 - Next Level: Ownership-based policy:

policies do
  policy action_type(:read) do
    authorize_if actor_attribute_equals(:role, :admin)
    authorize_if relates_to_actor_via(:reporter)
    authorize_if relates_to_actor_via(:assignee)
  end
end

Hint 3 - Technical Details: Custom check:

defmodule MyApp.Checks.Manager do
  use Ash.Policy.SimpleCheck

  def match?(actor, %{resource: Ticket} = _context, _opts) do
    actor.role in [:manager, :admin]
  end
end

Hint 4 - Verification: Test each policy by calling actions with different actors and verifying expected behavior.


Project 13: JSON:API with AshJsonApi

What You’ll Build

Create a fully compliant JSON:API from your Ash resources. Expose REST endpoints with proper serialization, filtering, and relationship handling.

Why This Project Matters

JSON:API is a specification for building APIs that reduces bikeshedding around response formats. AshJsonApi automatically generates compliant endpoints from your resource definitions, eliminating the need to write controllers, serializers, or documentation manually.

Core Challenges

  1. Installing and configuring AshJsonApi
  2. Exposing resource actions as endpoints
  3. Handling relationships in responses
  4. Filtering through query parameters
  5. OpenAPI documentation generation

Key Concepts

  • JSON:API specification
  • AshJsonApi router
  • Route configuration
  • Relationship inclusion
  • OpenAPI/Swagger generation
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 12 completed


Real-World Outcome

# List tickets
GET /api/tickets
{
  "data": [
    {
      "type": "ticket",
      "id": "uuid",
      "attributes": {
        "subject": "Help needed",
        "status": "open"
      },
      "relationships": {
        "reporter": {
          "data": {"type": "user", "id": "uuid"}
        }
      }
    }
  ]
}

# Create ticket
POST /api/tickets
Content-Type: application/vnd.api+json
{
  "data": {
    "type": "ticket",
    "attributes": {
      "subject": "New issue",
      "description": "Details here"
    },
    "relationships": {
      "reporter": {
        "data": {"type": "user", "id": "uuid"}
      }
    }
  }
}

# Filter tickets
GET /api/tickets?filter[status]=open&filter[priority]=high

# Include relationships
GET /api/tickets?include=reporter,comments

# Get OpenAPI spec
GET /api/openapi.json

The Core Question You’re Answering

How does AshJsonApi translate Ash’s declarative resource model into a fully compliant JSON:API, and how do you customize the API behavior while maintaining spec compliance?


Concepts You Must Understand First

  1. JSON:API spec: Resource objects, relationships, compound documents
  2. REST conventions: HTTP methods, status codes
  3. OpenAPI/Swagger: API documentation formats

Questions to Guide Your Design

  1. Which actions should be exposed via API?
  2. How should relationships be serialized?
  3. What filters should be available?
  4. How do you version your API?

Thinking Exercise

Design your API endpoints:

  • GET /api/tickets (list, filtered)
  • POST /api/tickets (create)
  • GET /api/tickets/:id (show)
  • PATCH /api/tickets/:id (update)
  • DELETE /api/tickets/:id (delete)
  • GET /api/tickets/:id/comments (related)
  • POST /api/tickets/:id/comments (create related)

What JSON:API features will each use?


The Interview Questions They’ll Ask

  1. “What is the JSON:API specification?”
  2. “How does AshJsonApi generate endpoints from resources?”
  3. “How do you handle authentication in AshJsonApi?”
  4. “How do you customize serialization?”
  5. “How do you generate OpenAPI documentation?”

Hints in Layers

Hint 1 - Starting Point: Install: mix igniter.install ash_json_api

Hint 2 - Next Level: Add to resource:

use Ash.Resource,
  extensions: [AshJsonApi.Resource]

json_api do
  type "ticket"

  routes do
    base "/tickets"
    get :read
    index :list
    post :create
    patch :update
    delete :destroy
  end
end

Hint 3 - Technical Details: Configure router:

defmodule MyAppWeb.Router do
  use AshJsonApi.Router,
    domains: [MyApp.Support],
    json_schema: "/json_schema",
    open_api: "/openapi"
end

Hint 4 - Verification: Test endpoints with curl or Postman. Verify JSON:API compliance with a validator.


Project 14: GraphQL with AshGraphQL

What You’ll Build

Create a GraphQL API from your Ash resources with queries, mutations, and subscriptions.

Why This Project Matters

GraphQL provides flexible querying where clients request exactly what they need. AshGraphQL automatically generates a GraphQL schema from your resources, including proper types, queries, mutations, and input validation. This eliminates the typical boilerplate of GraphQL implementations.

Core Challenges

  1. Installing and configuring AshGraphQL
  2. Generating queries and mutations
  3. Handling relationships and nested loading
  4. Custom fields and resolvers
  5. Subscriptions for real-time updates

Key Concepts

  • GraphQL schema generation
  • Queries and mutations
  • Input types
  • Relationship loading
  • Absinthe integration
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 12 completed, GraphQL basics


Real-World Outcome

# Query tickets with relationships
query {
  tickets(filter: {status: {eq: OPEN}}, sort: [{field: INSERTED_AT, order: DESC}]) {
    id
    subject
    status
    priority
    reporter {
      id
      email
    }
    comments {
      id
      body
      author {
        email
      }
    }
  }
}

# Create ticket mutation
mutation {
  createTicket(input: {
    subject: "Bug report"
    description: "App crashes"
    priority: HIGH
  }) {
    result {
      id
      subject
    }
    errors {
      field
      message
    }
  }
}

# Subscription
subscription {
  ticketCreated {
    id
    subject
    reporter {
      email
    }
  }
}

The Core Question You’re Answering

How does AshGraphQL leverage Ash’s introspectable resource model to generate a complete GraphQL schema with proper types, queries, mutations, and validation?


Concepts You Must Understand First

  1. GraphQL basics: Queries, mutations, subscriptions, types
  2. Absinthe: Elixir’s GraphQL library
  3. Schema design: How GraphQL schemas work

Questions to Guide Your Design

  1. Which actions should be exposed as queries/mutations?
  2. How should relationships be exposed?
  3. Should you enable subscriptions?
  4. How do you handle authentication in GraphQL?

Thinking Exercise

Design your GraphQL schema:

  • What queries are needed? (list, get, search)
  • What mutations? (create, update, close, assign)
  • What input types?
  • What return types?
  • Should field names match attributes or be transformed?

Write out the expected schema.


The Interview Questions They’ll Ask

  1. “How does AshGraphQL generate the schema from resources?”
  2. “How do you handle errors in GraphQL mutations?”
  3. “How do you implement authentication in AshGraphQL?”
  4. “What’s the N+1 problem and how does AshGraphQL handle it?”
  5. “How do you add custom resolvers?”

Hints in Layers

Hint 1 - Starting Point: Install: mix igniter.install ash_graphql

Hint 2 - Next Level: Add to resource:

use Ash.Resource,
  extensions: [AshGraphQL.Resource]

graphql do
  type :ticket

  queries do
    get :get, :read
    list :list, :list
  end

  mutations do
    create :create, :create
    update :update, :update
    destroy :destroy, :destroy
  end
end

Hint 3 - Technical Details: Configure schema:

defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  use AshGraphQL, domains: [MyApp.Support, MyApp.Accounts]

  query do
  end

  mutation do
  end
end

Hint 4 - Verification: Use GraphiQL to explore schema and test queries/mutations.


Project 15: Real-Time with PubSub and Notifiers

What You’ll Build

Implement real-time features using Ash notifiers and Phoenix.PubSub. Build live updates for ticket changes and new comments.

Why This Project Matters

Modern applications need real-time updates. Ash notifiers provide a declarative way to publish events when resources change. Combined with Phoenix.PubSub and LiveView, you can build reactive UIs that update automatically when data changes.

Core Challenges

  1. Configuring Ash.Notifier.PubSub
  2. Publishing events on actions
  3. Subscribing to topics in LiveView
  4. Handling real-time updates
  5. Managing topic patterns

Key Concepts

  • Ash.Notifier.PubSub
  • Phoenix.PubSub integration
  • Topic patterns
  • LiveView subscriptions
  • Event payloads
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Project 10, 14 completed


Real-World Outcome

# In resource - publish on actions
notifiers do
  pubsub MyApp.PubSub

  publish :create, ["tickets", :created]
  publish :update, ["tickets", :id, :updated]
  publish_all :update, ["tickets", :updated]
end

# In LiveView - subscribe and handle
def mount(_params, _session, socket) do
  if connected?(socket) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "tickets:created")
    Phoenix.PubSub.subscribe(MyApp.PubSub, "tickets:updated")
  end

  {:ok, assign(socket, tickets: list_tickets())}
end

def handle_info({:create, %Ash.Notifier.Notification{data: ticket}}, socket) do
  {:noreply, update(socket, :tickets, fn tickets -> [ticket | tickets] end)}
end

def handle_info({:update, %Ash.Notifier.Notification{data: ticket}}, socket) do
  {:noreply, update(socket, :tickets, fn tickets ->
    Enum.map(tickets, fn t ->
      if t.id == ticket.id, do: ticket, else: t
    end)
  end)}
end

The Core Question You’re Answering

How do Ash notifiers provide a declarative way to publish events on resource changes, and how do you build reactive UIs that respond to these events?


Concepts You Must Understand First

  1. Phoenix.PubSub: Pub/sub messaging in Elixir
  2. LiveView subscriptions: How LiveView handles external messages
  3. Topic patterns: Designing topic hierarchies

Questions to Guide Your Design

  1. What events should be published?
  2. What topic pattern makes sense?
  3. Who should receive which notifications?
  4. How do you handle notification payloads?

Thinking Exercise

Design your notification system:

  • Ticket created → notify all agents
  • Ticket updated → notify reporter and assignee
  • Comment added → notify ticket participants
  • Ticket assigned → notify new assignee

What topics and patterns do you need?


The Interview Questions They’ll Ask

  1. “How does Ash’s PubSub notifier work?”
  2. “What’s the difference between publish and publish_all?”
  3. “How do you handle notifications within transactions?”
  4. “How do you design topic patterns for multi-tenant apps?”
  5. “How do you test real-time notifications?”

Hints in Layers

Hint 1 - Starting Point: Add notifier to resource:

use Ash.Resource,
  notifiers: [Ash.Notifier.PubSub]

Hint 2 - Next Level: Configure publications:

pub_sub do
  module MyApp.PubSub
  prefix "tickets"

  publish :create, ["created"]
  publish :update, ["updated", :id]
end

Hint 3 - Technical Details: Subscribe in LiveView:

def mount(_params, _session, socket) do
  if connected?(socket) do
    :ok = Phoenix.PubSub.subscribe(MyApp.PubSub, "tickets:created")
    :ok = Phoenix.PubSub.subscribe(MyApp.PubSub, "tickets:updated:#{ticket_id}")
  end
  {:ok, socket}
end

Hint 4 - Verification: Open two browser tabs, create/update in one, verify changes appear in other.


Project 16: Testing Ash Resources

What You’ll Build

Comprehensive test suites for your Ash resources covering actions, policies, calculations, and integration scenarios.

Why This Project Matters

Testing Ash resources requires understanding what to test and how. Some things (like Ash internals) don’t need testing, but your business logic, policies, and custom code do. This project teaches testing patterns specific to Ash applications.

Core Challenges

  1. Setting up test infrastructure
  2. Testing actions with various inputs
  3. Testing policies with different actors
  4. Testing calculations and aggregates
  5. Integration testing with Phoenix

Key Concepts

  • Test data generation
  • Action testing patterns
  • Policy testing
  • Ash.Test helpers
  • ExUnit async testing
Difficulty: Intermediate Time: 4-5 hours

Prerequisites: Projects 1-15 understanding


Real-World Outcome

defmodule MyApp.Support.TicketTest do
  use MyApp.DataCase, async: true

  alias MyApp.Support.Ticket
  alias MyApp.Accounts.User

  describe "create action" do
    test "creates ticket with valid attributes" do
      user = create_user!()

      assert {:ok, ticket} =
        Ticket
        |> Ash.Changeset.for_create(:create, %{
          subject: "Test ticket",
          description: "Description",
          reporter_id: user.id
        })
        |> Ash.create()

      assert ticket.subject == "Test ticket"
      assert ticket.status == :open
    end

    test "fails without required subject" do
      user = create_user!()

      assert {:error, error} =
        Ticket
        |> Ash.Changeset.for_create(:create, %{
          reporter_id: user.id
        })
        |> Ash.create()

      assert Ash.Error.has_error?(error, field: :subject)
    end
  end

  describe "policies" do
    test "user can read own tickets" do
      user = create_user!()
      ticket = create_ticket!(reporter: user)

      assert {:ok, [found]} =
        Ticket
        |> Ash.Query.filter(id == ^ticket.id)
        |> Ash.read(actor: user)

      assert found.id == ticket.id
    end

    test "user cannot read others' tickets" do
      user1 = create_user!()
      user2 = create_user!()
      ticket = create_ticket!(reporter: user1)

      assert {:ok, []} =
        Ticket
        |> Ash.Query.filter(id == ^ticket.id)
        |> Ash.read(actor: user2)
    end
  end

  describe "calculations" do
    test "age calculation returns correct value" do
      ticket = create_ticket!(inserted_at: DateTime.add(DateTime.utc_now(), -2, :day))

      ticket = Ash.load!(ticket, :age)

      assert ticket.age == 2
    end
  end
end

The Core Question You’re Answering

What testing patterns are effective for Ash applications, and how do you test declarative resource definitions while avoiding testing the framework itself?


Concepts You Must Understand First

  1. ExUnit basics: Test structure, assertions, setup
  2. DataCase: Database test isolation
  3. Test factories: Generating test data

Questions to Guide Your Design

  1. What needs testing vs what’s framework behavior?
  2. How do you generate consistent test data?
  3. How do you test policies in isolation?
  4. How do you test async behavior?

Thinking Exercise

Design test cases for:

  1. Ticket creation (success, validation errors)
  2. Ticket state transitions (valid, invalid)
  3. Policy enforcement (owner, admin, stranger)
  4. Calculations (with various inputs)
  5. Aggregates (correct counts)

What fixtures/factories do you need?


The Interview Questions They’ll Ask

  1. “What should you test in an Ash resource?”
  2. “How do you test policies effectively?”
  3. “How do you set up test data for Ash tests?”
  4. “Should you test calculations that use expressions?”
  5. “How do you test real-time notifications?”

Hints in Layers

Hint 1 - Starting Point: Set up DataCase with Ash-aware database wrapping per AshPostgres testing guide.

Hint 2 - Next Level: Create test factories:

defmodule MyApp.Factory do
  def create_user!(attrs \\ %{}) do
    User
    |> Ash.Changeset.for_create(:create, Map.merge(%{
      email: "user#{System.unique_integer()}@test.com",
      password: "password123",
      password_confirmation: "password123"
    }, attrs))
    |> Ash.create!()
  end
end

Hint 3 - Technical Details: Test assertions:

# Check for specific error
assert Ash.Error.has_error?(error, field: :subject, message: "is required")

# Check policy failure
assert {:error, %Ash.Error.Forbidden{}} = result

Hint 4 - Verification: Run mix test and ensure all tests pass. Check coverage.


Project 17: Full Application - Help Desk System

What You’ll Build

A complete help desk application combining all concepts: resources, actions, policies, calculations, aggregates, Phoenix integration, APIs, real-time updates, and comprehensive testing.

Why This Project Matters

This capstone project integrates everything you’ve learned into a production-ready application. You’ll face the real challenges of combining features, handling edge cases, and building a cohesive user experience. This is where understanding deepens into mastery.

Core Challenges

  1. Domain modeling for a real application
  2. Complex policy requirements
  3. Multiple user interfaces (web, API)
  4. Real-time features
  5. Production considerations

Key Concepts

  • Full application architecture
  • Multi-domain organization
  • Feature integration
  • Production patterns
Difficulty: Advanced Time: 10-15 hours

Prerequisites: All previous projects


Real-World Outcome

A fully functional help desk with:

Web Interface (Phoenix LiveView):

  • User registration and authentication
  • Dashboard showing ticket counts and status
  • Ticket list with filtering, sorting, pagination
  • Ticket detail with comments and history
  • Real-time updates when tickets change
  • Agent assignment interface
  • Manager reporting dashboard

JSON:API:

  • Full CRUD for tickets via REST
  • Relationship handling
  • Filtering and pagination
  • OpenAPI documentation

GraphQL API:

  • Queries for tickets, users
  • Mutations for all actions
  • Subscriptions for real-time

Authorization:

  • Customers see only their tickets
  • Agents see assigned tickets
  • Managers see all tickets in department
  • Admins see everything

The Core Question You’re Answering

How do you architect a production Ash application that combines multiple domains, interfaces, and real-time features while maintaining code quality and testability?


Application Requirements

Users:

  • Registration, login, password reset
  • Roles: customer, agent, manager, admin
  • Profile management

Tickets:

  • Create, update, close, reopen
  • Priority levels
  • Status workflow (open → in_progress → resolved → closed)
  • Assignment to agents
  • SLA tracking

Comments:

  • Add comments to tickets
  • Internal notes (visible only to agents)
  • File attachments (optional)

Departments:

  • Organize agents and tickets
  • Department managers
  • Cross-department escalation

Reports:

  • Ticket volume by time period
  • Resolution time metrics
  • Agent performance

Questions to Guide Your Design

  1. How do you structure domains for this application?
  2. What resources belong in each domain?
  3. How do tickets flow through the workflow?
  4. What policies enforce role-based access?
  5. What real-time updates are needed?

Thinking Exercise

Before coding, design:

  1. Domain structure (which resources in which domain)
  2. Resource relationships (ER diagram)
  3. Action list for each resource
  4. Policy matrix (who can do what)
  5. API design (endpoints/queries)
  6. Real-time event design

Implementation Milestones

  1. Week 1: Core resources (User, Ticket, Comment)
  2. Week 2: Authentication, authorization policies
  3. Week 3: Phoenix LiveView interface
  4. Week 4: APIs (JSON:API, GraphQL)
  5. Week 5: Real-time updates, refinement, testing

Hints in Layers

Hint 1 - Starting Point: Create separate domains:

# lib/my_app/accounts.ex - User, Token, Role
# lib/my_app/support.ex - Ticket, Comment
# lib/my_app/organization.ex - Department

Hint 2 - Next Level: Use generators to scaffold:

mix ash.gen.resource MyApp.Accounts.User
mix ash.gen.resource MyApp.Support.Ticket --extend postgres
mix ash.gen.resource MyApp.Support.Comment --extend postgres

Hint 3 - Technical Details: Structure your LiveView:

lib/my_app_web/live/
├── ticket_live/
│   ├── index.ex
│   ├── show.ex
│   ├── form_component.ex
│   └── comment_component.ex
├── dashboard_live.ex
└── auth/
    ├── login_live.ex
    └── register_live.ex

Hint 4 - Verification: Create comprehensive integration tests covering main user flows.


Common Pitfalls & Debugging

Problem Cause Fix
Circular domain dependencies Resources referencing across domains Use string references, reorganize domains
N+1 queries Missing relationship preloading Use Ash.Query.load/2
Policy confusion Complex rules interacting Test policies in isolation first
Real-time not working Transaction timing Notifications are sent after transaction
Slow tests Not using async Use async: true with proper isolation

Project Comparison Table

Project Difficulty Time Core Skill Dependencies
P1: Installation & First Resource Beginner 2-3h Setup, basics None
P2: Attributes and Types Beginner 2-3h Data modeling P1
P3: Relationships Beginner-Int 3-4h Data modeling P1-2
P4: CRUD Actions Intermediate 3-4h Behavior P1-3
P5: Custom Actions Intermediate 4-5h Business logic P4
P6: AshPostgres Intermediate 3-4h Persistence P1-5
P7: Calculations Intermediate 3-4h Derived data P6
P8: Aggregates Intermediate 3-4h Summary data P7
P9: Filtering/Sorting Intermediate 3-4h Querying P8
P10: AshPhoenix Forms Intermediate 4-5h UI integration P9
P11: Authentication Intermediate 4-5h Security P10
P12: Authorization Int-Advanced 5-6h Security P11
P13: JSON:API Intermediate 4-5h API design P12
P14: GraphQL Intermediate 4-5h API design P12
P15: PubSub/Notifiers Intermediate 4-5h Real-time P10, P14
P16: Testing Intermediate 4-5h Quality P1-15
P17: Full Application Advanced 10-15h Integration All

Recommendation

For Beginners to Ash

Start with Projects 1-6 to build a solid foundation. These cover the core concepts that everything else builds on. Don’t rush - understanding resources, actions, and the data layer is essential.

For Phoenix Developers New to Ash

Focus on understanding how Ash differs from traditional Phoenix patterns. Pay special attention to:

  • Resources vs Ecto schemas + contexts
  • Actions vs controller logic
  • Policies vs scattered authorization
  • AshPhoenix.Form vs Ecto changesets

For Production Applications

Prioritize these projects:

  1. P12 (Authorization) - Security is non-negotiable
  2. P16 (Testing) - Quality assurance
  3. P6 (PostgreSQL) - Production persistence
  4. P11 (Authentication) - User management

Learning Path Summary

                    ┌─────────────────────────────────────────┐
                    │           Week 1-2                       │
                    │  P1 → P2 → P3 → P4 → P5 → P6            │
                    │  (Core Resources & Data Layer)           │
                    └───────────────────┬─────────────────────┘
                                        │
                    ┌───────────────────▼─────────────────────┐
                    │           Week 3-4                       │
                    │  P7 → P8 → P9 → P10                     │
                    │  (Queries & UI Integration)              │
                    └───────────────────┬─────────────────────┘
                                        │
                    ┌───────────────────▼─────────────────────┐
                    │           Week 5-6                       │
                    │  P11 → P12 → P13 → P14                  │
                    │  (Auth & APIs)                          │
                    └───────────────────┬─────────────────────┘
                                        │
                    ┌───────────────────▼─────────────────────┐
                    │           Week 7-8                       │
                    │  P15 → P16 → P17                        │
                    │  (Real-time, Testing, Capstone)         │
                    └─────────────────────────────────────────┘

Summary

Project What You Build Key Skill Gained Time
P1 Basic resource Ash fundamentals 2-3h
P2 Rich attributes Data modeling 2-3h
P3 Related resources Relationship modeling 3-4h
P4 CRUD operations Action design 3-4h
P5 Custom workflows Business logic encapsulation 4-5h
P6 Postgres integration Production persistence 3-4h
P7 Calculated fields Derived data 3-4h
P8 Summary data Efficient aggregations 3-4h
P9 Query builder Data access patterns 3-4h
P10 Phoenix forms UI integration 4-5h
P11 User auth Identity management 4-5h
P12 Access control Security policies 5-6h
P13 REST API JSON:API spec 4-5h
P14 GraphQL API Flexible queries 4-5h
P15 Real-time updates Reactive patterns 4-5h
P16 Test suites Quality assurance 4-5h
P17 Full help desk Everything combined 10-15h

Resources

Official Documentation

Books

Community

Tutorials


Last updated: 2025-12-29