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
- See layer-interactions.md for layer communication patterns
- See service-patterns.md for service object patterns
- See query-patterns.md for query object patterns
- See error-handling.md for error handling strategies
- See testing-strategy.md for comprehensive testing