VOOZH about

URL: https://dev.to/adamanq/ash-framework-introduction-3lm3

⇱ Ash Framework: Introduction - DEV Community


When a newcomer starts with Elixir and Phoenix, they are almost always introduced to Ecto first. You create schemas, write changesets, and move queries into contexts. This approach is flexible, but as the project grows, a serious problem begins to emerge 😟

Business logic starts to spread across dozens of files. Eventually, confusion arises about where to place request handling code and how to make sense of the existing mess. After a year or two, it becomes difficult to understand where the β€œright” place for code is, and new developers spend weeks just getting up to speed with the project.

Ash Framework was created precisely to solve this pain πŸ’ͺ

Important clarification: Ash is not a replacement for Phoenix and not a new web framework. It is a powerful declarative layer that organizes business logic and data on top of the existing ecosystem.

γ…€


γ…€

πŸ“– Preface

It’s important to understand that the goal of this article is not to convince you to use Ash everywhere and all the time!

Personally, I love Ash because it allows you to gather all necessary logic in a single space, where it belongs. This tool significantly simplifies work, reducing boilerplate code and freeing you from the tedious implementation of standard CRUD.

If you’re ready to get acquainted with such a tool but plan to use it only when truly justifiedβ€”read on without hesitation. Ash is not a universal cure-all, but rather a convenient set of useful tools.

γ…€


γ…€

πŸš€ What is Ash?

Ash is a framework for fast and efficient application development in Elixir. Its goal is to simplify launching new projects and make maintaining existing solutions convenient, ensuring excellent performance and stability even after long periods of active development.

The key idea of Ash is:

Precisely define the structure of your business logic, and let Ash handle all the rest (database migrations, API interface creation, data validation, access control, form generation).

The framework integrates beautifully with popular tools like Phoenix, PostgreSQL, GraphQL, and LiveView.

γ…€


γ…€

πŸ”‘ Key Abstractions in Ash

Everything in Ash is built around three main concepts:

  • Resource β€” description of a single business entity
  • Action β€” an operation that can be performed on a resource
  • Domain β€” logical grouping of resources within one business area

Let’s break down each concept in detail!

γ…€


γ…€

🧩 What is a Resource?

Resource is one module where a single business entity of your application (User, Lesson, Order, Post, Payment, etc.) is fully described.

Everything related to this entity is collected in one place:

  • Data structure (fields)
  • Relationships with other entities
  • Operations that can be performed (CRUD + custom)
  • Validation rules
  • Authorization (who can do what)
  • Computed fields
  • Indexes and constraints
  • Storage settings in the database

A Resource in Ash unifies in one module what in classic Elixir/Phoenix is usually spread across several parts: Ecto Schema, Changesets, Context functions, as well as authorization logic and computed fields.

γ…€

Resource β€” The Heart of Ash

Ash is built around the idea of Resource-Oriented Design. You describe what an entity is and what can be done with itβ€”and Ash generates the rest: SQL queries, APIs, forms, migrations, validation, etc.

γ…€

Complete Example of a Modern Resource

defmodule CourseHub.Courses.Lesson do
 use Ash.Resource,
 otp_app: :course_hub,
 domain: CourseHub.Courses,
 data_layer: AshPostgres.DataLayer

 # ==================== STORAGE ====================
 postgres do
 table "lessons"
 repo CourseHub.Repo
 end

 # ==================== FIELDS (Attributes) ====================
 attributes do
 uuid_primary_key :id

 attribute :title, :string do
 public? true
 allow_nil? false
 constraints min_length: 3, max_length: 200
 end

 attribute :description, :string, public?: true
 attribute :content, :string, public?: true

 attribute :status, :atom do
 public? true
 default? :draft
 constraints one_of: [:draft, :published, :archived]
 end

 create_timestamp :inserted_at
 update_timestamp :updated_at
 end

 # ==================== RELATIONSHIPS ====================
 relationships do
 belongs_to :author, CourseHub.Accounts.User do
 public? true
 allow_nil? false
 end

 has_many :comments, CourseHub.Courses.Comment do
 public? true
 end

 many_to_many :tags, CourseHub.Courses.Tag do
 public? true
 through CourseHub.Courses.LessonTag
 source_attribute_on_join_resource :lesson_id
 destination_attribute_on_join_resource :tag_id
 end
 end

 # ==================== ACTIONS ====================
 actions do
 defaults [:create, :read, :update, :destroy]

 # Custom read
 read :published do
 filter expr(status == :published)
 pagination offset?: true, countable: true
 prepare build(sort: [inserted_at: :desc])
 end

 # Custom create with logic
 create :publish do
 argument :reason, :string
 change set_attribute(:status, :published)
 end

 # Fully custom action
 action :calculate_reading_time, :integer do
 argument :content, :string, allow_nil?: false
 run fn input, _context ->
 minutes = String.length(input.arguments.content) |> div(500) |> max(1)
 {:ok, minutes}
 end
 end
 end

 # ==================== COMPUTED FIELDS ====================
 calculations do
 calculate :reading_time_minutes, :integer do
 calculation fn records, _context ->
 Enum.map(records, fn lesson ->
 (String.length(lesson.content || "") / 500) |> Float.ceil() |> trunc()
 end)
 end
 end
 end

 # ==================== INDEXES ====================
 identities do
 identity :unique_title_per_author, [:title, :author_id]
 end

 # ==================== AUTHORIZATION ====================
 policies do
 policy action_type(:read) do
 authorize_if always()
 end

 policy action(:update) do
 authorize_if expr(author_id == ^actor(:id))
 end
 end
end

γ…€

Main Sections of Resource

Section Describes Analog in Classic Elixir/Phoenix
attributes Fields and their types + validation Ecto Schema + fields
relationships Relationships with other resources Ecto associations
actions All possible operations (CRUD + custom) Context functions + Changesets
calculations Computed fields (on the fly) Virtual fields / functions
aggregates Aggregates (count, sum, avg, etc.) Queries with group_by
identities Unique indexes Ecto unique constraints
policies Authorization rules Manual permission checks
postgres PostgreSQL storage settings Migrations

γ…€

Key Features of Resource

  1. Everything in one file β€” a huge plus for understanding the entity.
  2. Introspection β€” Ash can read all information about a resource at runtime (fields, actions, policies). This is used for API generation, forms, admin panels.
  3. Declarativeness β€” you describe what should be there, not how to implement it.
  4. Extensibility β€” you can add your own changes, preparations, validations, notifiers.
  5. Embedded Resources β€” you can create resources stored not in a separate table but as JSON/Map within another resource.

γ…€

When to Create a New Resource?

Almost always when a new business entity appears (a table or even a complex JSON object).

γ…€


γ…€

πŸ› οΈ More About Actions

Action is an explicitly declared operation that can be performed on a resource.
All data interaction in Ash goes only through Actions. You cannot simply take a record and change it manuallyβ€”you always call an action.

This replaces:

  • Regular functions in Phoenix Contexts
  • Changesets in Ecto
  • Controllers and service layers

γ…€

Five Types of Actions

Action Type What It Does? In Transaction? Returns?
✨ create Creates a new record Yes New resource record
πŸ” read Reads one record or a list No (can be enabled) Record or list of records
✏️ update Updates an existing record Yes Updated record
πŸ—‘οΈ destroy Deletes a record Yes Deleted record
⚑ action (Generic) Any custom business logic No (can be enabled) Any value

γ…€

Complete Example of All Action Types

defmodule CourseHub.Courses.Lesson do
 use Ash.Resource, ...

 actions do
 # 1. Standard CRUD (defaults)
 defaults [:create, :read, :update, :destroy]

 # 2. Custom Read
 read :published do
 filter expr(status == :published)
 pagination offset?: true, countable: true, default_limit: 20
 prepare build(sort: [inserted_at: :desc], load: [:author, :comment_count])
 end

 read :for_user do
 argument :user_id, :uuid, allow_nil?: false
 filter expr(author_id == ^arg(:user_id))
 end

 # 3. Custom Create
 create :publish do
 argument :reason, :string, allow_nil?: true

 # Changes (changes)
 change set_attribute(:status, :published)
 change set_attribute(:published_at, &DateTime.utc_now/0)
 change {CourseHub.Changes.NotifySubscribers, topic: "new_lessons"}

 # Hooks
 after_action fn _changeset, lesson, _context ->
 {:ok, lesson}
 end
 end

 # 4. Custom Update
 update :archive do
 accept [:archive_reason]
 change set_attribute(:status, :archived)
 change set_attribute(:archived_at, &DateTime.utc_now/0)
 end

 # 5. Generic Action (fully custom)
 action :calculate_reading_time, :integer do
 argument :content, :string, allow_nil?: false
 argument :words_per_minute, :integer, default?: 200

 run fn input, _context ->
 minutes = String.length(input.arguments.content) 
 |> div(input.arguments.words_per_minute) 
 |> max(1)
 {:ok, minutes}
 end
 end
 end
end

γ…€

How to Call Actions

# Through Domain (recommended way)
CourseHub.Courses.publish!(lesson, %{reason: "Π“ΠΎΡ‚ΠΎΠ²ΠΎ"})
CourseHub.Courses.read!(CourseHub.Courses.Lesson, :published)
# Or with Ash.Query / Ash.Changeset for complex cases
query = Ash.Query.for_read(Lesson, :published)
Ash.read!(query)

γ…€

Main Capabilities of Actions

1. argument β€” Input Parameters

Allows passing data into an action and validating it.

argument :title, :string, allow_nil?: false, constraints: [min_length: 5]
argument :published_at, :datetime, default?: &DateTime.utc_now/0
argument :user_id, :uuid, allow_nil?: false

2. accept β€” Which Attributes Can Be Changed

Limits the list of fields that can be modified during create or update. Very useful for security.

create :register do
 accept [:email, :password, :name]
end

update :update_profile do
 accept [:name, :bio, :avatar_url]
end

3. change β€” Data Transformations

The most powerful and frequently used part of Actions. Here the main business logic happens.

create :publish do
 argument :reason, :string

 change set_attribute(:status, :published)
 change set_attribute(:published_at, &DateTime.utc_now/0)

 change {CourseHub.Changes.NotifySubscribers, topic: "lessons"}

 change fn changeset, _context ->
 Ash.Changeset.change_attribute(changeset, :slug, generate_slug(changeset))
 end
end

4. prepare β€” Query Preparation (mainly for read)

Allows pre-configuring the query: sorting, preloading relations, additional filters.

read :published do
 filter expr(status == :published)

 prepare build(
 sort: [inserted_at: :desc],
 load: [:author, :comments],
 limit: 20
 )
end

5. Hooks (before/after)

Allows executing code before or after an action.

create :publish do
 before_action fn changeset, _context ->
 validate_business_rules(changeset)
 end

 after_action fn _changeset, lesson, _context ->
 LessonNotifier.publish_event(lesson)
 {:ok, lesson}
 end

 after_transaction fn
 {:ok, lesson}, _context -> broadcast_new_lesson(lesson)
 {:error, _}, _ -> :ok 
 end
end

6. transaction? β€” Transaction Management

Determines whether the action should be wrapped in a database transaction.

action :transfer_money, :boolean do
 argument :from_account_id, :uuid
 argument :to_account_id, :uuid
 argument :amount, :decimal

 transaction? true

 run fn input, _context ->
 # complex operation with multiple records
 {:ok, true}
 end
end

7. pipe_through β€” Reusing Logic

Allows creating common pipelines and attaching them to different actions (very useful for DRY).

# In Resource
pipelines do
 pipeline :ensure_admin do
 change ensure_actor_role(:admin)
 validate actor_role(:admin)
 end
end

create :create_admin_content do
 pipe_through :ensure_admin
 accept [:title, :content]
end

update :update_admin_content do
 pipe_through :ensure_admin
 accept [:title, :content]
end

γ…€

Why Actions Are Awesome?

  • Full typing and introspection β€” Ash knows everything about an action at runtime (used for GraphQL, forms, admin panels).
  • Declarativeness β€” code reads like business documentation.
  • Reusability β€” one action can be used in LiveView, API, Background Job, Tests.
  • Security β€” authorization policies are tied to specific actions.
  • Testability β€” easy to test individual actions.

γ…€

Comparison with Classic Elixir/Phoenix

Approach Where is logic? Number of files Clarity
Ecto + Context Context + Changeset 4–8 Medium
Ash Actions In one Resource 1 High

Actions are where all your business logic lives.

γ…€


γ…€

🏒 What is a Domain?

Domain is a logical container that groups related Resources.
It is similar to Phoenix Context, but much more powerful. If Resource describes one entity (like User, Lesson, Order), then Domain is a "module" or "bounded context" (in DDD terms) that brings together several entities of one business area.

Example:

  • Accounts β€” domain for users, profiles, authorization.
  • Courses β€” domain for courses, lessons, tests, enrollments.
  • Billing β€” domain for payments, subscriptions, invoices.
  • Therapy β€” domain for therapy sessions, conversations, messages.

γ…€

Three Main Goals of Domain

  1. Grouping Resources Organizing the project. Instead of all resources being scattered, they are explicitly grouped into logical units.
  2. Centralized Code Interface Domain allows defining convenient wrapper functions for calling resource actions.
  3. Shared Settings and Cross-Cutting Concerns Here you can set behavior that applies to all resources in the domain (timeouts, default pagination, authorization, etc.).

γ…€

How Domain Looks in Practice

defmodule CourseHub.Courses do
 use Ash.Domain, otp_app: :course_hub

 # 1. Grouping resources
 resources do
 resource CourseHub.Courses.Course
 resource CourseHub.Courses.Lesson
 resource CourseHub.Courses.Quiz
 resource CourseHub.Courses.Enrollment
 end

 # 2. Code Interface β€” convenient functions
 define :get_lesson, action: :read, args: [:id], get?: true
 define :list_published_courses, action: :published
 define :enroll_user, action: :create, resource: CourseHub.Courses.Enrollment

 # 3. Shared settings
 execution do
 timeout :timer.seconds(30)
 default_page_size 20
 end

 # Enabling AshAdmin (admin panel)
 admin do
 show? true
 end
end

γ…€

Why Do You Need Domain?

  • Organization and boundaries. Clearly separates different parts of the application (like bounded contexts in DDD). Accounts doesn't know about Billing without explicit relations.
  • Convenient public API. Instead of Ash.read!(CourseHub.Courses.Lesson, ...), you write beautifully:
CourseHub.Courses.get_lesson!(lesson_id)
CourseHub.Courses.list_published_courses!()
  • Centralized management. You can disable authorization for the entire domain in dev mode, set common policies, notifiers, etc.
  • Introspection and extensions. Many Ash extensions (GraphQL, JsonApi, Phoenix) work at the domain level.
  • Testing and isolation. Easier to test one business area as a whole.

γ…€

Best Practices When Working with Domains

  • One resource β€” one domain. A resource cannot belong to multiple domains simultaneously.
  • How many domains to make?
    • Small project β†’ 1–3 domains.
    • Medium/large β†’ one per major business area (Accounts, Core, Billing, Notifications, etc.). This helps with scaling and understanding architecture.
  • Code Interface β€” a very powerful feature. You can define not only simple wrappers but also those with preset filters, loads, etc.

γ…€

Comparison: Domain vs Resource

Aspect Resource Domain
What it describes One entity Group of entities
Where logic lies Attributes, actions, policies Grouping + shared settings
Code call Through domain Public app interface
Analog in Phoenix Schema + Context (partially) Phoenix Context
Quantity per project Many (one per table) Few (per business area)

γ…€

Why Domain Is Especially Useful?

  • Project grows >10–15 resources.
  • Complex authorization (policies) needed.
  • Doing GraphQL / JSON:API / Admin panel.
  • Team development β€” everyone needs to understand boundaries.

γ…€


γ…€

βš”οΈ Ash + Phoenix vs Ecto + Phoenix

Aspect Phoenix + Ecto (classic) 😐 Phoenix + Ash πŸ”₯ Who wins?
Business logic organization Scattered across Context, Changeset, separate modules Everything in one Resource Ash
CRUD operations Write manually: create, read, update, delete + boilerplate defaults [:create, :read, :update, :destroy] β€” done ✨ Ash
Authorization Manual checks / Bodyguard / scopes Declarative Policies (secure by default) ⚑ Ash
Computed fields Virtual fields + functions in Context Calculations β€” auto-loaded and cached Ash
Number of files per entity 3–6+ files (Schema + Context + Changeset + Policy) One file β€” Resource Ash
API generation Absinthe / GraphQL manually + lots of code AshGraphql or AshJsonApi β€” add extension and done Ash
LiveView forms Phoenix.Component + manual validation AshPhoenix.Form β€” forms know everything themselves Ash
Multi-tenancy Write yourself (or use schemas) Built into Policies (attribute-based or context-based) Ash
Maintainability after 2–3 years Medium (logic spreads) High (everything declarative and in one place) Ash
Development speed Normal at start Much faster after learning curve πŸš€ Ash
Flexibility Maximum (full control) Very high + can escape to pure Ecto when needed Draw
Learning curve Easier for beginners Steeper but pays off in large projects Ecto

γ…€

What to Choose in 2026?

Pros of Ash πŸ”₯:

  • Declarativeness β†’ "Describe the domain β€” Ash does the rest."
  • Boilerplate reduction by orders of magnitude ("I write code 3–5 times faster").
  • Automatic generation of GraphQL, JSON:API, Admin panel, authentication.
  • Authorization policies and multi-tenancy out of the box.
  • Excellent maintainability in the long run (exactly what production dreams of). Pros of classic Phoenix + Ecto 😐:
  • Full control over every byte of code.
  • Simpler for very small projects and pet projects.
  • No extra abstraction (if you love "bare" Elixir).
  • Easier for Elixir beginners. When to choose what?
  • Small pet project or experiment β†’ pure Phoenix + Ecto.
  • Medium/large project, SaaS, long-lived app β†’ Phoenix + Ash (huge time and nerve savings).
  • Want both β†’ you can safely mix in one project (Ash and regular Ecto coexist perfectly).

γ…€


γ…€

😴 Conclusion

Ash is not just a library but a shift in approach to Elixir development. It especially shines in medium and large projects where clean architecture, development speed, and long-term maintainability are important.
Of course, it has a learning curve and may be excessive for very simple pet projects. But if you're tired of spreading logic and want a clear declarative structureβ€”Ash gives a very pleasant sense of control over your code.

γ…€


γ…€

πŸ™ From the Author

Thank you very much for your interest in this article! I hope it helped you understand what Ash is and why it's used.
If you liked this article and want more materials like thisβ€”join me on my Telegram channel, where I post book reviews, Elixir publications, technical literature, and interesting news!