Not organization but coordination.

Part of a series on the Ash Framework book. Previous: My Misconceptions

Ash Framework

Learn how to use Ash Framework to build robust, maintainable Elixir applications. This book covers the principles and practices of domain-driven design with Ash's declarative approach.

A traditional framework provides its own stack, such as an ORM, view layer, routing system, and its own approach to validation, authorization, and testing. It defines how an application should be structured and expects us to work within those boundaries. This brings consistency, but at the cost of flexibility.

Ash is different. Instead of building its own stack, it coordinates well known libraries in the Elixir ecosystem.

What Ash Discovered

Ash recognized that much of the work in Elixir applications comes down to wiring libraries together. Defining attributes, setting up validations, exposing APIs, and enforcing policies. More importantly, those layers all depend on the same core information.

The Elixir ecosystem had already embraced Domain Driven Design through patterns like Phoenix contexts and Ecto repos. Ash builds on that foundation, letting us declare resources and actions at the domain level and generating the code needed to keep everything in sync.

The Coordination Problem

Adding a field is easy. Writing field :dob, :date takes seconds. The real cost is keeping all the layers aligned.

  • Ecto: schemas and migrations
  • LiveView: form logic and authorization
  • JSON API: endpoints and serialization
  • GraphQL: schema types and resolvers
  • Tests, docs, and fixtures for each of them

Schema definition

field :dob, :date

Validation

|> cast(attrs, [:name, :dob])
|> validate_past_date(:dob)

Authorization

def can_edit_dob?(user, artist) do
  # Who can update DOB? Same rules as name? Different?
end

Interfaces

LiveView

<%= if can_edit_dob?(@current_user, @artist) do %>
  <%= input f, :dob, type: :date %>
<% end %>

JSON API

def show(conn, %{"id" => id}) do
  artist = get_artist!(id)
  dob = if can_view_dob?(conn.assigns.current_user, artist),
    do: artist.dob, else: nil
  render(conn, "show.json", %{artist: %{artist | dob: dob}})
end

GraphQL

field :dob, :date do
  resolve fn artist, _, %{context: %{current_user: user}} ->
    if can_view_dob?(user, artist), do: {:ok, artist.dob}, else: {:ok, nil}
  end
end

Each module makes sense on its own, but miss a step and things drift. A form allows edits that should not be permitted. An API leaks information. A migration goes untested. The complexity is not in the code itself. It is in keeping everything aligned.

Ash’s Solution

# In Artist resource
attributes do
  attribute :dob, :date do
    public? true
  end
end

# Authorization through action policies (Chapter 6)
policies do
  policy action(:update) do
    authorize_if actor_attribute_equals(:role, :admin)
  end
end

Then we run a few commands:

mix ash.codegen
mix ash_postgres.generate_migrations
mix ecto.migrate

Ash generates:

  • Ecto migrations and schemas
  • JSON and GraphQL endpoints
  • LiveView form helpers
  • Authorization logic

Instead of editing scattered files across multiple layers, we change the resource definition and let Ash handle the coordination. There’s no lock-in. We can still drop down into the primary libraries or even write an adapter to integrate a new one.

This softens the convention cliff found in other frameworks. With Ash, stepping outside simply means returning to familiar tools.