RageArch
Clean Architecture Light for Rails. Business logic in testable use cases, thin controllers, and models free of callbacks.
Core concepts
- Controllers only orchestrate — no business logic
- Models stay clean — no business callbacks
- Use cases hold all logic, with injected dependencies and typed results
Setup
bundle install
rails g rage_arch:installCreates config/initializers/rage_arch.rb, app/use_cases/, app/deps/, and injects include RageArch::Controller into ApplicationController.
Components
RageArch::Result — typed result object
result = RageArch::Result.success(order)
result.success? # => true
result.value # => order
result = RageArch::Result.failure(["Validation error"])
result.failure? # => true
result.errors # => ["Validation error"]
RageArch::Container — dependency registration
# Register by instance
RageArch.register(:order_store, MyApp::Deps::OrderStore.new)
# Register with a block (lazy evaluation)
RageArch.register(:mailer) { Mailer.new }
# Register an ActiveRecord model as dep (wraps it automatically)
RageArch.register_ar(:user_store, User)
# Resolve
RageArch.resolve(:order_store) # => the registered implementation
# Check if registered
RageArch.registered?(:order_store) # => trueDependencies (Deps)
A dep is any object that a use case needs from the outside world: persistence, mailers, external APIs, caches, etc. No base class required — any Ruby object can be a dep.
Writing a dep manually
# app/deps/posts/post_store.rb
module Posts
class PostStore
def build(attrs = {})
Post.new(attrs)
end
def save(record)
record.save
end
def find(id)
Post.find_by(id: id)
end
def list(filters: {})
Post.where(filters).to_a
end
end
endRegister it in config/initializers/rage_arch.rb:
RageArch.register(:post_store, Posts::PostStore.new)ActiveRecord dep (generated)
For deps that simply wrap an AR model with standard CRUD, use the generator:
rails g rage_arch:ar_dep post_store PostThis creates app/deps/posts/post_store.rb with build, find, save, update, destroy, and list methods backed by RageArch::Deps::ActiveRecord.for(Post).
Generating a dep from use case analysis
rails g rage_arch:dep post_storeScans your use cases for method calls on :post_store and generates a class with stub methods for each one. If the file already exists, only missing methods are added.
Switching dep implementations
Use dep_switch to swap between multiple implementations of the same dep:
# Interactive — lists all available implementations and prompts you to choose
rails g rage_arch:dep_switch post_store
# Direct — activate a specific implementation
rails g rage_arch:dep_switch post_store PostgresPostStore
# Switch to ActiveRecord adapter
rails g rage_arch:dep_switch post_store arThe generator scans app/deps/ for files matching the symbol, updates config/initializers/rage_arch.rb by commenting out the old registration and adding the new one.
RageArch::UseCase::Base — use cases
Now that you know how Result, Container, and Deps work, use cases tie them together. A use case declares its dependencies by symbol, receives them via injection, and returns a Result:
class CreateOrder < RageArch::UseCase::Base
use_case_symbol :create_order
deps :order_store, :notifications # injected by symbol from the Container
def call(params = {})
order = order_store.build(params)
return failure(order.errors) unless order_store.save(order)
notifications.notify(:order_created, order)
success(order)
end
endBuild and run manually:
use_case = RageArch::UseCase::Base.build(:create_order)
result = use_case.call(reference: "REF-1", total_cents: 1000)
ar_dep — inline ActiveRecord dep
When a dep is a simple wrapper over an ActiveRecord model, declare it directly in the use case instead of creating a separate class:
class Posts::Create < RageArch::UseCase::Base
use_case_symbol :posts_create
ar_dep :post_store, Post # auto-creates an AR adapter if :post_store is not registered
def call(params = {})
post = post_store.build(params)
return failure(post.errors.full_messages) unless post_store.save(post)
success(post: post)
end
endIf :post_store is registered in the container, that implementation is used. Otherwise, RageArch::Deps::ActiveRecord.for(Post) is used as fallback.
RageArch::Controller — thin controller mixin
def create
run :users_register, register_params,
success: ->(r) { session[:user_id] = r.value[:user].id; redirect_to root_path, notice: "Created." },
failure: ->(r) { flash_errors(r); render :new, status: :unprocessable_entity }
end-
run(symbol, params, success:, failure:)— runs the use case and calls the matching block -
run_result(symbol, params)— runs and returns theResultdirectly -
flash_errors(result)— setsflash.now[:alert]fromresult.errors
API controller example (JSON):
class Api::PostsController < ApplicationController
def create
run :posts_create, post_params,
success: ->(r) { render json: r.value[:post], status: :created },
failure: ->(r) { render json: { errors: r.errors }, status: :unprocessable_entity }
end
end
RageArch::EventPublisher — domain events
Every use case automatically publishes an event when it finishes. Other use cases subscribe to react:
class Notifications::SendPostCreatedEmail < RageArch::UseCase::Base
use_case_symbol :send_post_created_email
deps :mailer
subscribe :posts_create # runs when :posts_create event is published
def call(payload = {})
return success unless payload[:success]
mailer.send_post_created(payload[:value][:post])
success
end
endSubscribe to multiple events or everything:
subscribe :post_created, :post_updated
subscribe :all # payload includes :event with the event nameOpt out of auto-publish for a specific use case:
skip_auto_publishOrchestration — use cases calling use cases
class CreateOrderWithNotification < RageArch::UseCase::Base
use_case_symbol :create_order_with_notification
deps :order_store
use_cases :orders_create, :notifications_send
def call(params = {})
result = orders_create.call(params)
return result unless result.success?
notifications_send.call(order_id: result.value[:order].id, type: :order_created)
result
end
endGenerators
| Command | What it does |
|---|---|
rails g rage_arch:install |
Initial setup (initializer, directories, controller mixin) |
rails g rage_arch:scaffold Post title:string |
Full CRUD: model, use cases, dep, controller, views, routes |
rails g rage_arch:scaffold Post title:string --api |
Same but API-only (JSON responses) |
rails g rage_arch:scaffold Post title:string --skip-model |
Skip model/migration if it already exists |
rails g rage_arch:use_case CreateOrder |
Generates a base use case file |
rails g rage_arch:use_case orders/create |
Generates a namespaced use case (Orders::Create) |
rails g rage_arch:dep post_store |
Generates a dep class by scanning method calls in use cases |
rails g rage_arch:ar_dep post_store Post |
Generates a dep that wraps an ActiveRecord model |
rails g rage_arch:dep_switch post_store |
Lists implementations and switches which one is registered |
rails g rage_arch:dep_switch post_store PostgresPostStore |
Directly activates a specific implementation |
Testing
# spec/rails_helper.rb
require "rage_arch/rspec_matchers"
require "rage_arch/fake_event_publisher"Result matchers:
expect(result).to succeed_with(post: a_kind_of(Post))
expect(result).to fail_with_errors(["Not found"])Fake event publisher:
publisher = RageArch::FakeEventPublisher.new
RageArch.register(:event_publisher, publisher)
# ... run use case ...
expect(publisher.published).to include(hash_including(event: :post_created))
publisher.clearConfiguration
# config/application.rb or config/initializers/rage_arch.rb
# Disable automatic event publishing when use cases finish (default: true)
config.rage_arch.auto_publish_events = false
# Disable boot verification (default: true)
config.rage_arch.verify_deps = falseBoot verification
At boot, RageArch.verify_deps! runs automatically and raises if it finds wiring problems. It checks:
- Every dep declared with
deps :symbolis registered in the container - Every method called on a dep is implemented by the registered object (via static analysis)
- Every use case declared with
use_cases :symbolexists in the registry
Example error output:
RageArch boot verification failed:
UseCase :posts_create (Posts::Create) declares dep :post_store — not registered in container
UseCase :posts_create (Posts::Create) calls :post_store#save — Posts::PostStore does not implement #save
UseCase :posts_notify (Posts::Notify) declares use_cases :email_send — not registered in use case registry
Disable with config.rage_arch.verify_deps = false.
Instrumentation
Every use case emits "rage_arch.use_case.run" via ActiveSupport::Notifications with payload symbol, params, success, errors, result.
ActiveSupport::Notifications.subscribe("rage_arch.use_case.run") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.info "[UseCase] #{event.payload[:symbol]} (#{event.duration.round}ms) success=#{event.payload[:success]}"
endDocumentation
-
doc/GETTING_STARTED.md— Getting started guide with common tasks -
doc/DOCUMENTATION.md— Detailed behaviour (use cases, deps, events, config) -
doc/REFERENCE.md— Quick-lookup API reference (classes, methods, options)
AI context
If you use an AI coding agent, point it to IA.md for a compact reference of the full gem API, conventions, and architecture.
License
MIT