Modern Rails 8 Architecture Patterns
Architecture Decision Tree
Where should this code go?
|
+- View/display formatting? -> Presenter (@presenter-agent)
+- Complex business logic? -> Service Object (@service-agent)
+- Complex database query? -> Query Object (@query-agent)
+- Shared behavior across models? -> Concern (/rails-concern skill)
+- Authorization logic? -> Policy (@policy-agent)
+- Reusable UI with logic? -> ViewComponent (@viewcomponent-agent)
+- Async/background work? -> Job (@job-agent, /solid-queue-setup skill)
+- Complex form (multi-model)? -> Form Object (@form-agent)
+- Transactional email? -> Mailer (@mailer-agent)
+- Real-time/WebSocket? -> Channel (/action-cable-patterns skill)
+- Data validation only? -> Model (@model-agent)
+- HTTP request/response only? -> Controller (@controller-agent)
Layer Responsibilities
| Layer |
Responsibility |
Should NOT contain |
| Controller |
HTTP, params, response |
Business logic, queries |
| Model |
Data, validations, relations |
Display logic, HTTP |
| Service |
Business logic, orchestration |
HTTP, display logic |
| Query |
Complex database queries |
Business logic |
| Presenter |
View formatting, badges |
Business logic, queries |
| Policy |
Authorization rules |
Business logic |
| Component |
Reusable UI encapsulation |
Business logic |
| Job |
Async processing |
HTTP, display logic |
| Form |
Complex form handling |
Persistence logic |
| Mailer |
Email composition |
Business logic |
| Channel |
WebSocket communication |
Business logic |
When NOT to Abstract
| Situation |
Keep It Simple |
Don't Create |
| Simple CRUD (< 10 lines) |
Keep in controller |
Service object |
| Used only once |
Inline the code |
Abstraction |
| Simple query with 1-2 conditions |
Model scope |
Query object |
| Basic text formatting |
Helper method |
Presenter |
| Single model form |
form_with model: |
Form object |
| Simple partial without logic |
Partial |
ViewComponent |
When TO Abstract
| Signal |
Action |
| Same code in 3+ places |
Extract to concern/service |
| Controller action > 15 lines |
Extract to service |
| Model > 300 lines |
Extract concerns |
| Complex conditionals |
Extract to policy/service |
| Query joins 3+ tables |
Extract to query object |
| Form spans multiple models |
Extract to form object |
See /extraction-timing skill for detailed extraction guidance.
Core Patterns
Skinny Controllers
# GOOD: Thin controller delegates to service
class OrdersController < ApplicationController
def create
result = Orders::CreateService.call(user: current_user, params: order_params)
if result.success?
redirect_to result.data, notice: t(".success")
else
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
end
end
Result Objects for Services
All services return a consistent Result object:
Result = Data.define(:success, :data, :error) do
def success? = success
def failure? = !success
end
Multi-Tenancy by Default
# GOOD: Scoped through account
def index
@events = current_account.events.recent
end
Rails 8 Specific Features
| Feature |
Purpose |
Skill/Agent |
| Authentication |
has_secure_password generator |
/authentication-flow |
| Background Jobs |
Solid Queue (database-backed) |
/solid-queue-setup, @job-agent |
| Real-time |
Action Cable + Solid Cable |
/action-cable-patterns |
| Caching |
Solid Cache (database-backed) |
/caching-strategies |
| Assets |
Propshaft + Import Maps |
(built-in) |
| Deployment |
Kamal 2 + Thruster |
(built-in) |
Testing Strategy by Layer
| Layer |
Test Type |
Focus |
| Model |
Unit |
Validations, scopes, methods |
| Service |
Unit |
Business logic, edge cases |
| Query |
Unit |
Query results, tenant isolation |
| Presenter |
Unit |
Formatting, HTML output |
| Controller |
Request |
Integration, HTTP flow |
| Component |
Component |
Rendering, variants |
| Policy |
Unit |
Authorization rules |
| System |
E2E |
Critical user paths |
New Feature Checklist
- Model - Define data structure (@migration-agent, @model-agent)
- Policy - Add authorization rules (@policy-agent)
- Service - Create for complex logic (@service-agent)
- Query - Add for complex queries (@query-agent)
- Controller - Keep it thin (@controller-agent)
- Presenter - Format for display (@presenter-agent)
- Component - Build reusable UI (@viewcomponent-agent)
- Mailer - Add transactional emails (@mailer-agent)
- Job - Add background processing (@job-agent)
References