Ash Framework: My Misconceptions
It’s all about the domain.
When I first heard about Ash, I assumed it was “Rails for Elixir,” a set of conventions to reduce boilerplate and get an app moving quickly. Rails is great at that, but as projects grow beyond the conventions, it can be harder to adapt without extra workarounds.
After ElixirConf I decided I needed to know more, so I picked up the Ash Framework book.
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.
From Persistence to Domain
Ash frames its approach around three principles:
- Prefer data over code
- Prefer deriving logic over hand-writing it
- Prefer describing what should happen over how to do it
When we design a system, we usually begin with high-level concepts: the things we want to model and how they relate. Most frameworks then have us translate those ideas into persistence concerns such as tables, APIs, or controllers. Ash challenges that assumption by asking: what if we modeled the domain itself, and let everything else follow?
How This Plays Out in Practice
Ash uses code generation — a lot of code generation. Most of the book is about teaching us Ash’s code generation patterns.
One-time Scaffolds
mix ash.gen.resource
creates a new resource module with placeholder DSL sections. This pattern appears repeatedly: Album
in Chapter 2, Track
in Chapter 8, ArtistFollower
in Chapter 9, and Notification
in Chapter 10. Ash includes other generators of the same kind such as mix ash.gen.domain
, mix ash.gen.enum
, and mix ash.gen.validation
. These reduce initial typing, but once generated, the code is ours to maintain.
Repeatable Generators
mix ash_phoenix.gen.live
, introduced in Chapter 8, is similar to Phoenix’s LiveView generator, but it derives its fields directly from an Ash resource. Because it draws from the resource definition, we can re-run it as the resource evolves. That makes it practical for prototyping, though each regeneration overwrites styling and layout changes, so it is best to stabilize the data layer first.
Ongoing Regeneration
mix ash.codegen
regenerates supporting code based on the current state of our resources. In ash_postgres, for example, it doesn’t emit empty migration templates like Ecto. It compares our resource definitions to the existing schema and generates migration logic for the differences. Each run creates a new migration file, preserving history. Other extensions take a different approach. The TypeScript generator, for instance, overwrites artifacts like type definitions so they always match the current state.
Compile-time Macros
Ash also generates code through macros at compile time. Unlike CLI generators, which leave behind files we edit, macros inject functions directly into our modules whenever we recompile. The principle is the same: declare intent once, and the framework expands that intent into a family of imperative functions. The lifecycle differs, however. CLI tools create permanent scaffolds, while macro-based generation produces ephemeral code that is recreated on each compile.
Metaprogramming and Convention
Rails and Ash both use metaprogramming to turn declarative definitions into imperative methods. The difference is timing: Rails uses Ruby’s runtime metaprogramming, while Ash uses Elixir’s compile-time macros.
In Rails, an association like this:
has_many :posts
expands into helpers such as user.posts
and user.posts.build
. We don’t write those ourselves; they’re defined by convention.
In Ash, an action like this:
create :create
expands into functions like create_artist/2
and can_create_artist?/1
. Again, we don’t write them; recompilation injects them into our domain module.
Model Your Domain
Rails, Django, and Spring all start from a data model. You declare a table, a model, or an entity, and the framework generates controllers, query helpers, and views outward from persistence.
Ash starts from the domain. The book describes it as a way to “describe and build the domain model of your applications—the ‘things’ that make up what your app is supposed to do, and the business logic of how they relate and interact with each other.” One of its core principles is “Derive > Hand-write.” Or as the book asks: “Why should you need to restate your application logic in five different ways?”
So yes, Ash definitely feels inspired by Rails. But what makes it different, and frankly more interesting, is that it starts from the domain.