rails-architecture
Use when implementing or reviewing Rails application structure, MVC boundaries, resource flows, REST behavior, or cross-layer feature changes. Also covers maintainability review of REST design, MVC re
SKILL.mdskills/rails-architecture/SKILL.md
GitHubRails Architecture
Purpose
Map a Rails feature to the repository's existing request, domain, persistence, presentation, and integration boundaries.
Activate when
- a feature crosses routes/controllers/models/views/services
- a new Rails resource is introduced
- an existing endpoint grows in scope
- application boundaries are unclear
- architecture is being refactored
Boundary with focused Rails skills
Use this skill for cross-layer ownership and request-flow decisions. Delegate detailed route decisions to rails-routing, controller boundary decisions to rails-action-controller, persistence decisions to rails-active-record, and HTTP contract testing to rails-test-engineering. Do not duplicate detailed guidance from those skills.
Patterns
This skill consults the following patterns from the shared patterns/ catalog. The pattern names are load-bearing — in a single-skill install, the catalog is not present, so an agent names the pattern to load rather than linking to its path.
| Load when | Pattern | Covers |
|---|---|---|
| a change crosses routes/controllers/models/views/services and needs the cross-layer flow traced | request-flow |
Request lifecycle; HTTP contract boundary |
| a change introduces or moves a dependency between layers | dependency-direction-contract |
Dependency direction; inward-pointing boundaries |
| a controller and a service share authorization for one operation | controller-service-authorization-composition |
Authorization at the execution boundary, not only the controller |
| a policy object is introduced or moved | policy-object-boundary |
Policy object responsibility and isolation |
| a change should be audited against the Rails Way philosophy (CoC, DRY, MVC, use-Rails-first, deliberate deviation) | the-rails-way |
5-step decision hierarchy for AI coding assistants; where-code-belongs placement table; eight anti-patterns mapped to owning skills/patterns |
Repository inspection
Read the smallest useful slice of the repository:
routes
-> controller
-> authorization/authentication
-> domain/service/query
-> model/database
-> serializer/view
-> tests
Also inspect:
- Gemfile/lockfile
- schema/migrations
- CI
- existing conventions
- background jobs/external services when involved
MVC responsibilities
Rails' MVC model separates request handling, presentation, and persistence/domain behavior.
A controller should coordinate the request, not become a second domain layer.
Views should present.
Persistence models should represent persistence and domain behavior that naturally belongs there.
Services/domain objects/query objects should be introduced when the workflow or query responsibility is genuinely separate from the existing object.
Thin controller / model responsibility
The source material describes the traditional "thin controller, richer model" Rails style. Preserve the intent—controllers should not contain substantial business logic—but do not interpret "fat model" as permission to turn Active Record models into catch-alls.
Repository conventions decide whether domain behavior belongs in models, services, form objects, policies, commands, or other boundaries.
REST/resource design
Prefer conventional resources when they accurately represent the operation.
Use custom actions when standard resource semantics do not fit, and keep them explicit.
Cross-layer procedure
- trace the current request flow
- identify the required behavior contract
- identify the owner of each responsibility
- reuse existing boundaries
- implement the smallest coherent change
- update focused tests
- verify authorization, validation, persistence, and response behavior
Architectural anti-patterns
- fat controllers
- Active Record models used as universal service objects
- duplicated business rules across controller/model/view
- introducing a service for a trivial single-object operation
- inventing a new layer when the repository has no need for it
- mixing unrelated refactors into a feature
Reference example
Request flow layered by responsibility: the HTTP boundary stays stateless and thin, the workflow owns the transaction, the model owns invariants.
# app/controllers/checkouts_controller.rb - HTTP boundary only
class CheckoutsController < ApplicationController
def create
result = Checkout::Complete.call(user: current_user, cart: current_cart)
if result.success?
redirect_to result.value, notice: t(".completed")
else
redirect_to cart_path, alert: result.error
end
end
end
# app/operations/checkout/complete.rb - workflow boundary (transaction, collaborators)
# app/models/order.rb - persistence and invariants
#
# Dependencies point inward: controller -> operation -> model.
# A model that needs checkout steps is the wrong direction; re-route through the
# operation instead of reaching outward from the persistence layer.
Agent review checklist
- request flow understood
- boundary ownership explicit
- existing conventions reused
- controller remains orchestration-focused
- persistence concerns are not duplicated
- cross-layer tests exist where the contract crosses layers
- architecture change is justified by actual complexity
Verification
Trace the final feature end-to-end and run the appropriate request/model/system tests. Inspect the diff for responsibility leakage and accidental architectural expansion.
Source foundation
Grounded in the MVC, Rails application anatomy, REST/CRUD, and Rails philosophy material in The Ruby Workshop, including DRY and convention-over-configuration. The boundary discipline is strengthened using Clean Ruby's responsibility and refactoring guidance.
Book integration: request lifecycle
For a cross-layer change, reason through the complete path:
route -> authentication -> authorization -> params -> application/domain operation -> persistence -> representation -> HTTP response.
REST resources should use conventional resource routes when the semantics fit. Custom actions are justified by actual domain operations, not by controller convenience.
The traditional thin-controller/richer-domain guidance is useful, but do not turn Active Record models into universal workflow containers. Existing repository boundaries still decide where behavior belongs.
Rails code-quality review
Merged from the retired rails-architecture skill.
Repository inspection
Inspect:
- Rails/Ruby versions
- config/routes.rb
- controllers, models, views/helpers
- migrations/schema
- associations/scopes
- tests
- services/jobs where relevant
- config/rails_best_practices.yml if used
- repository conventions
Do not apply a check mechanically without understanding the application's contract.
Review domains
Model and persistence
Review missing database indexes, risky default_scope, misplaced finder/query logic, duplicated relationship traversal, query attributes, duplicated model logic, unnecessary model methods, and input-protection concerns appropriate to the Rails version.
RESTful routes
Review excessive custom actions, needless deep nesting, default/catch-all routes, and unrestricted auto-generated routes. Prefer conventional resources when they accurately represent the operation.
Controllers
Review business logic leakage, repetitive setup, render complexity, and unused actions. Do not move business logic merely to satisfy a metric.
Views and helpers
Review business/data-access logic in templates, presentation logic that belongs in helpers/presenters, unnecessary instance-variable exposure, complex rendering, and empty/unused helpers.
Migrations and seed data
Review indexes, seed/application data separation, migration safety, and production impact. Historical rules must be adapted to the current Rails/database environment.
Error handling
Do not rescue Exception broadly. Catch the narrowest recoverable exception at the appropriate boundary.
Mailers
Review multipart representation when the application's mail contract requires multiple content alternatives.
Dead code
Treat unused methods as review signals. Search callers, reflection, routes, callbacks, jobs, and external consumers before removal.
Pattern-selection guidance
Treat analyzer findings as signals: detect -> inspect context -> identify risk -> choose smallest justified fix -> test -> verify
Do not turn historical rules into absolute laws. In particular, do not interpret 'move to model' as permission for fat models, 'use before filter' as permission for business workflows in callbacks, or 'use association/scope/factory' as a mandate to introduce those abstractions everywhere.
Modern Rails compatibility
Some original checks reflect older Rails conventions such as before_filter, legacy mass-assignment APIs, and Turbo Sprockets-era asset behavior. Translate the underlying intent to the application's actual Rails version. Never introduce deprecated APIs to satisfy a historical rule.
Reference example
The controller/contributor split the linter generation expects: assignment and response in the controller, the workflow in a callable object.
class OrdersController < ApplicationController
def create
result = PlaceOrder.call(user: current_user, cart: current_cart)
if result.success?
redirect_to result.value, notice: t(".created")
else
flash.now[:alert] = result.error
render :checkout, status: :unprocessable_entity
end
end
end
class PlaceOrder
def self.call(user:, cart:) = new(user: user, cart: cart).call
def initialize(user:, cart:)
@user = user
@cart = cart
end
def call
order = @user.orders.create!(line_items_attributes: @cart.line_item_attributes)
Result.success(order)
rescue ActiveRecord::RecordInvalid => e
Result.failure(e.record.errors.full_messages.to_sentence)
end
end
Agent review checklist
- Rails/Ruby version resolved
- relevant checks considered
- findings interpreted in repository context
- indexes/constraints considered
- REST route surface reviewed
- controller/model/view/helper ownership checked
- callbacks justified
- migrations reviewed for production safety
- exception handling is narrow
- unused-code claims verified
- historical rules translated to modern Rails
- no pattern introduced solely to satisfy a metric
Verification
Run the repository's configured RailsBestPractices command when installed and compatible. Also run focused tests and relevant CI checks. Treat analyzer output as review input: resolve, suppress with documented justification, or intentionally accept findings according to repository policy. Never claim a clean run unless it was actually executed.