Architecture checks for Ruby and Rails.
See the codeExecutable architecture specifications for Ruby and Rails
Declare your components and boundaries in one Archspec.rb, and every change gets checked in CI, whether a person or a coding agent wrote it. No AI involved, just static analysis.
ArchSpec turns your architecture into executable checks: components, layers, constant references, inheritance, mixins, named method calls, method protocols, naming conventions, cycles, and Rails boundaries. Checking is plain static analysis: Rubydex builds the semantic index, Prism supplies a small syntax overlay, the app never boots during a check, and no AI is involved in checking your code.
Architecture usually lives in pull request comments, onboarding docs, and senior engineers' heads. That does not scale well, especially when code is moving fast. More of that code is now written by coding agents that do not know your conventions.
ArchSpec gives you a small Ruby DSL for the rules that code review otherwise has to remember, and checks them on every change:
ArchSpec does not try to infer the "true" design pattern of arbitrary Ruby code. You describe the architecture your team wants. ArchSpec checks whether the code still matches it.
Start with conventional Rails boundaries:
architecture :rails
Go vanilla, 37signals style, with rich models and no service objects:
architecture :vanilla_rails
Add layers when the app has a clear direction of dependencies:
architecture :layered
Override the default directories when the app uses different names:
architecture :layered, layers: {
interface: "app/controllers/**/*.rb",
application: "app/services/**/*.rb",
domain: "app/models/**/*.rb"
}
Keep a hexagonal core away from adapters:
architecture :hexagonal
Check a modular monolith, and make packs go through a public API:
architecture :modular_monolith,
components: {
billing: "packs/billing/**/*.rb",
catalog: "packs/catalog/**/*.rb",
shared: "packs/shared/**/*.rb"
},
allow: {
billing: %i[shared],
catalog: %i[shared]
},
public: {
billing: "packs/billing/app/public/**/*.rb"
}
Write local rules in plain Ruby:
component :controllers, in: "app/controllers/**/*.rb"
component :models, in: "app/models/**/*.rb"
component :services, in: "app/services/**/*.rb"
component :records, descendants_of: "ApplicationRecord"
controllers.can_only_use :models, :services
models.cannot_use :controllers
services.cannot_call :render, :redirect_to, :params, :session,
because: "services do not own the HTTP response"
models.cannot_call :find_by_sql, receiver: "ActiveRecord::Base"
services.cannot_instantiate_and_invoke
Check command/query separation:
architecture :cqrs,
commands: "app/commands/**/*.rb",
queries: "app/queries/**/*.rb",
read_models: "app/read_models/**/*.rb"
call(amount, actor:)get_/set_ or pairing with_x with without_xSomething.new(...).whatever command objectsapp/services in vanilla RailsArchSpec does not check Zeitwerk constant names. Zeitwerk does that itself. Add Zeitwerk::Loader.eager_load_all (or your loader's eager_load) to your test suite or CI. It raises on any file that does not define the constant its path implies, using your real inflector and ignores.
ArchSpec requires Ruby 3.2 or newer.
Add ArchSpec to your Gemfile:
group :development, :test do
gem "archspec"
end
Then install it:
bundle install
Create Archspec.rb:
bundle exec archspec init
Run the checks:
bundle exec archspec check
bundle exec archspec init
bundle exec archspec check
bundle exec archspec check --format json
bundle exec archspec check --update-todo
bundle exec archspec explain app/models/user.rb
To check dependencies Rails resolves at runtime, such as association targets
and custom validators, add reflect :rails to Archspec.rb and run
bundle exec archspec reflect --environment test. It boots the app once, in a
separate process, and saves what Rails resolved. Checks read those facts
without booting anything, and fail with facts.stale for files edited since.
Locally, archspec check --reflect refreshes stale facts before checking.
See the Rails reflection guide.
Other frameworks and macro libraries can supply facts with a small producer that the same command runs. See the framework integration guide.
explain shows why a file or constant belongs to a component, its resolved
ancestry, outgoing facts, incoming dependencies, and anything the analysis
could not prove:
app/models/user.rb
defined constants: User
components:
models: matched file pattern app/models/**/*.rb
outgoing facts:
1:14 │ inherits from ApplicationRecord
2:3 │ references UsersController
Generated code should pass the same architecture checks as hand-written code.
After an AI-assisted change, check the whole project, or just the files that changed:
bundle exec archspec check
bundle exec archspec check app/models/user.rb app/services
Passing paths still analyzes the project so dependencies resolve, but reports only violations in those paths. This keeps the loop tight after each edit.
If it fails, read the evidence before changing the spec:
[error] models must not depend on controllers [dependencies.forbid]
app/models/user.rb:2:3
1 │ class User < ApplicationRecord
→ 2 │ UsersController
│ ^~~~~~~~~~~~~~~
3 │ end
note: User references UsersController
Most failures should be fixed in the generated code. Update the spec only when the architecture decision itself has changed.
Use a todo file when adopting ArchSpec in an existing app. It records the current violations so they stop failing the build, leaving a list to burn down:
todo "archspec_todo.yml"
bundle exec archspec check --update-todo
In CI, add --check-todo so a fixed violation that is still listed fails the
build instead of rotting in the file:
bundle exec archspec check --check-todo
Use local suppressions for deliberate exceptions:
# archspec:disable-next-line dependencies.forbid -- legacy admin export
Admin::UsersController
Read the guides at archspecrb.dev.
This repository checks its own architecture:
bundle exec rake architecture
It also runs against pinned checkouts of large real-world Rails apps to catch crashes and false positives before they ship:
bundle exec rake torture
Released under the MIT License.
242 followers · starred Aug 2026
1,138 followers · starred Jun 2026
488 followers · starred Jul 2026
218 followers · starred Jun 2026
Architecture checks for Ruby and Rails.
See the codeExecutable architecture specifications for Ruby and Rails
Declare your components and boundaries in one Archspec.rb, and every change gets checked in CI, whether a person or a coding agent wrote it. No AI involved, just static analysis.
ArchSpec turns your architecture into executable checks: components, layers, constant references, inheritance, mixins, named method calls, method protocols, naming conventions, cycles, and Rails boundaries. Checking is plain static analysis: Rubydex builds the semantic index, Prism supplies a small syntax overlay, the app never boots during a check, and no AI is involved in checking your code.
Architecture usually lives in pull request comments, onboarding docs, and senior engineers' heads. That does not scale well, especially when code is moving fast. More of that code is now written by coding agents that do not know your conventions.
ArchSpec gives you a small Ruby DSL for the rules that code review otherwise has to remember, and checks them on every change:
ArchSpec does not try to infer the "true" design pattern of arbitrary Ruby code. You describe the architecture your team wants. ArchSpec checks whether the code still matches it.
Start with conventional Rails boundaries:
architecture :rails
Go vanilla, 37signals style, with rich models and no service objects:
architecture :vanilla_rails
Add layers when the app has a clear direction of dependencies:
architecture :layered
Override the default directories when the app uses different names:
architecture :layered, layers: {
interface: "app/controllers/**/*.rb",
application: "app/services/**/*.rb",
domain: "app/models/**/*.rb"
}
Keep a hexagonal core away from adapters:
architecture :hexagonal
Check a modular monolith, and make packs go through a public API:
architecture :modular_monolith,
components: {
billing: "packs/billing/**/*.rb",
catalog: "packs/catalog/**/*.rb",
shared: "packs/shared/**/*.rb"
},
allow: {
billing: %i[shared],
catalog: %i[shared]
},
public: {
billing: "packs/billing/app/public/**/*.rb"
}
Write local rules in plain Ruby:
component :controllers, in: "app/controllers/**/*.rb"
component :models, in: "app/models/**/*.rb"
component :services, in: "app/services/**/*.rb"
component :records, descendants_of: "ApplicationRecord"
controllers.can_only_use :models, :services
models.cannot_use :controllers
services.cannot_call :render, :redirect_to, :params, :session,
because: "services do not own the HTTP response"
models.cannot_call :find_by_sql, receiver: "ActiveRecord::Base"
services.cannot_instantiate_and_invoke
Check command/query separation:
architecture :cqrs,
commands: "app/commands/**/*.rb",
queries: "app/queries/**/*.rb",
read_models: "app/read_models/**/*.rb"
call(amount, actor:)get_/set_ or pairing with_x with without_xSomething.new(...).whatever command objectsapp/services in vanilla RailsArchSpec does not check Zeitwerk constant names. Zeitwerk does that itself. Add Zeitwerk::Loader.eager_load_all (or your loader's eager_load) to your test suite or CI. It raises on any file that does not define the constant its path implies, using your real inflector and ignores.
ArchSpec requires Ruby 3.2 or newer.
Add ArchSpec to your Gemfile:
group :development, :test do
gem "archspec"
end
Then install it:
bundle install
Create Archspec.rb:
bundle exec archspec init
Run the checks:
bundle exec archspec check
bundle exec archspec init
bundle exec archspec check
bundle exec archspec check --format json
bundle exec archspec check --update-todo
bundle exec archspec explain app/models/user.rb
To check dependencies Rails resolves at runtime, such as association targets
and custom validators, add reflect :rails to Archspec.rb and run
bundle exec archspec reflect --environment test. It boots the app once, in a
separate process, and saves what Rails resolved. Checks read those facts
without booting anything, and fail with facts.stale for files edited since.
Locally, archspec check --reflect refreshes stale facts before checking.
See the Rails reflection guide.
Other frameworks and macro libraries can supply facts with a small producer that the same command runs. See the framework integration guide.
explain shows why a file or constant belongs to a component, its resolved
ancestry, outgoing facts, incoming dependencies, and anything the analysis
could not prove:
app/models/user.rb
defined constants: User
components:
models: matched file pattern app/models/**/*.rb
outgoing facts:
1:14 │ inherits from ApplicationRecord
2:3 │ references UsersController
Generated code should pass the same architecture checks as hand-written code.
After an AI-assisted change, check the whole project, or just the files that changed:
bundle exec archspec check
bundle exec archspec check app/models/user.rb app/services
Passing paths still analyzes the project so dependencies resolve, but reports only violations in those paths. This keeps the loop tight after each edit.
If it fails, read the evidence before changing the spec:
[error] models must not depend on controllers [dependencies.forbid]
app/models/user.rb:2:3
1 │ class User < ApplicationRecord
→ 2 │ UsersController
│ ^~~~~~~~~~~~~~~
3 │ end
note: User references UsersController
Most failures should be fixed in the generated code. Update the spec only when the architecture decision itself has changed.
Use a todo file when adopting ArchSpec in an existing app. It records the current violations so they stop failing the build, leaving a list to burn down:
todo "archspec_todo.yml"
bundle exec archspec check --update-todo
In CI, add --check-todo so a fixed violation that is still listed fails the
build instead of rotting in the file:
bundle exec archspec check --check-todo
Use local suppressions for deliberate exceptions:
# archspec:disable-next-line dependencies.forbid -- legacy admin export
Admin::UsersController
Read the guides at archspecrb.dev.
This repository checks its own architecture:
bundle exec rake architecture
It also runs against pinned checkouts of large real-world Rails apps to catch crashes and false positives before they ship:
bundle exec rake torture
Released under the MIT License.
242 followers · starred Aug 2026
1,138 followers · starred Jun 2026
488 followers · starred Jul 2026
218 followers · starred Jun 2026