layered-rails
Layered Rails
Design and review Rails applications using layered architecture principles.
Quick Start
Rails applications are organized into four architecture layers with unidirectional data flow:
┌─────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ Controllers, Views, Channels, Mailers │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ APPLICATION LAYER │
│ Service Objects, Form Objects, etc. │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ DOMAIN LAYER │
│ Models, Value Objects, Domain Events │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ Active Record, APIs, File Storage │
└─────────────────────────────────────────┘
Core Rule: Lower layers must never depend on higher layers.
See Architecture Layers Reference for the full layer responsibilities and the Four Rules deep-dive.
What Would You Like To Do?
- Analyze codebase - Run
/layers:analyzefor full analysis or/layers:analyze:services,/layers:analyze:callbacks,/layers:analyze:godsfor specific checks - Review code changes - Run
/layers:reviewfor layered architecture review - Run specification test - Run
/layers:spec-teston specific files - Plan gradual adoption - Run
/layers:gradual [goal]to plan incremental layerification - Plan feature implementation - I'll guide you using layered principles
- Implement specific pattern - I'll help with authorization, notifications, view components, AI integration, etc.
Core Principles
The Four Rules
- Unidirectional Data Flow - Data flows top-to-bottom only
- No Reverse Dependencies - Lower layers never depend on higher layers
- Abstraction Boundaries - Each abstraction belongs to exactly one layer
- Minimize Connections - Fewer inter-layer connections = looser coupling
Common Violations
| Violation | Example | Fix |
|---|---|---|
| Model uses Current | Current.user in model |
Pass user as explicit parameter |
| Service accepts request | param :request in service |
Extract value object from request |
| Controller has business logic | Pricing calculations in action | Extract to service or model |
| Anemic models | All logic in services | Keep domain logic in models |
| Category | Reference |
|---|---|
| Layer violations (Current in models, request in services, notifications in models, business logic in controllers) | layer-violations.md |
| Service objects (anemic models, bag of random objects, premature abstraction) | service-objects.md |
| Callbacks (operation callbacks, skip callbacks, control flags) | callbacks.md |
| Concerns (code-slicing, overgrown) | concerns.md |
| Helpers (HTML construction in helpers) | helpers.md |
| Jobs (anemic jobs) | jobs.md |
| Testing (testing wrong layer) | testing.md |
The Specification Test
If the specification of an object describes features beyond the primary responsibility of its abstraction layer, such features should be extracted into lower layers.
How to apply:
- List responsibilities the code handles
- Evaluate each against the layer's primary concern
- Extract misplaced responsibilities to appropriate layers
See Specification Test Reference for detailed guide.
Pattern Catalog
| Pattern | Layer | Use When | Reference |
|---|---|---|---|
| Service Object | Application | Orchestrating domain operations | service-objects.md |
| Query Object | Domain | Complex, reusable queries | query-objects.md |
| Form Object | Presentation | Multi-model forms, complex validation | form-objects.md |
| Filter Object | Presentation | Request parameter transformation | filter-objects.md |
| Presenter | Presentation | View-specific logic, multiple models | presenters.md |
| Serializer | Presentation | API response formatting | serializers.md |
| Policy Object | Application | Authorization decisions | policy-objects.md |
| Value Object | Domain | Immutable, identity-less concepts | value-objects.md |
| Collaborator Object | Domain | A slice of one model's behavior in a typed delegate | collaborator-objects.md |
| State Machine | Domain | States, events, transitions | state-machines.md |
| Concern | Domain | Shared behavioral extraction | concerns.md |
| Repository | Application | Last resort — returning custom domain objects mapped from AR data, after AR scopes (simple) and query objects (query building) are insufficient | repositories.md |
Pattern Selection Guide
"Where should this code go?"
| If you have... | Consider... |
|---|---|
| Complex multi-model form | Form Object |
| Request parameter filtering/transformation | Filter Object |
| View-specific formatting | Presenter |
| Complex database query used in multiple places | Query Object |
| Business operation spanning multiple models | Service Object (as waiting room) |
| Authorization rules | Policy Object |
| Multi-channel notifications | Delivery Object (Active Delivery) |
Remember: Services are a "waiting room" for code until proper abstractions emerge. Don't let app/services become a bag of random objects.
Refactoring Scenarios
Canonical before/after transformations for the most common layerification moves. The /layers:gradual agent uses these as reference templates when proposing phases.
| Scenario | Goal area | Reference |
|---|---|---|
| Extract callbacks to service | callbacks, after_create chains | callbacks-to-service.md |
| Extract authorization to policy | authorization, permissions | authorization-to-policy.md |
| Extract query logic to query object | complex scopes, reporting queries | query-to-query-object.md |
| Extract Current from model | Current.* in domain | current-from-model.md |
| Decompose god object with associated objects | god model, large User/Account | god-object-decomposition.md |
| Replace implicit state machine | timestamp-based status | implicit-to-explicit-state-machine.md |
| Extract view logic to presenter | template logic, formatting | view-logic-to-presenter.md |
| Form object for complex input | fat controllers, multi-model forms | complex-input-to-form-object.md |
Commands Reference
| Command | Purpose |
|---|---|
/layers:review |
Review code changes from layered architecture perspective |
/layers:spec-test |
Run specification test on specific files |
/layers:analyze |
Full codebase abstraction layer analysis |
/layers:analyze:services |
Audit app/services/ and service-like classes — conventions, clusters, layer hygiene, test consequences |
/layers:analyze:callbacks |
Score model callbacks, find extraction candidates |
/layers:analyze:gods |
Find God objects via churn × complexity |
/layers:gradual [goal] |
Plan gradual adoption of layered patterns |
Topic References
For deep dives on specific topics:
| Topic | Reference |
|---|---|
| Authorization (RBAC, ABAC, policies) | authorization.md |
| Notifications (multi-channel delivery) | notifications.md |
| View Components | view-components.md |
| AI Integration (LLM, agents, RAG, MCP) | ai-integration.md |
| Configuration | configuration.md |
| Callbacks (scoring, extraction) | callbacks.md |
| Current Attributes | current-attributes.md |
| Instrumentation (logging, metrics) | instrumentation.md |
Gem References
For library-specific guidance:
| Gem | Purpose | Reference |
|---|---|---|
| action_policy | Authorization framework | action-policy.md |
| view_component | Component framework | view-component.md |
| anyway_config | Typed configuration | anyway-config.md |
| active_delivery | Multi-channel notifications | active-delivery.md |
| alba | JSON serialization | alba.md |
| workflow | State machines | workflow.md |
| rubanok | Filter/transformation DSL | rubanok.md |
| active_agent | AI agent framework | active-agent.md |
| active_job-performs | Eliminate anemic jobs | active-job-performs.md |
Extraction Signals
When to extract from models:
| Signal | Metric | Action |
|---|---|---|
| God object | High churn × complexity | Decompose into concerns, delegates, or separate models |
| Operation callback | Score 1-2/5 | Extract to service or event handler |
| Code-slicing concern | Groups by artifact type | Convert to behavioral concern or extract |
| Current dependency | Model reads Current.* | Pass as explicit parameter |
Callback Scoring:
| Type | Score | Keep? |
|---|---|---|
| Transformer (compute values) | 5/5 | Yes |
| Normalizer (sanitize input) | 4/5 | Yes |
| Utility (counter caches) | 4/5 | Yes |
| Observer (side effects) | 2/5 | Maybe |
| Operation (business steps) | 1/5 | Extract |
See Extraction Signals Reference for detailed guide.
Model Organization
Recommended order within model files:
class User < ApplicationRecord
# 1. Gems/DSL extensions
has_secure_password
# 2. Associations
belongs_to :account
has_many :posts
# 3. Enums
enum :status, { pending: 0, active: 1 }
# 4. Normalization
normalizes :email, with: -> { _1.strip.downcase }
# 5. Validations
validates :email, presence: true
# 6. Scopes
scope :active, -> { where(status: :active) }
# 7. Callbacks (transformers only)
before_validation :set_defaults
# 8. Delegations
delegate :name, to: :account, prefix: true
# 9. Public methods
def full_name = "#{first_name} #{last_name}"
# 10. Private methods
private
def set_defaults
self.locale ||= I18n.default_locale
end
end
Success Checklist
Well-layered code:
- No reverse dependencies (lower layers don't depend on higher)
- Models don't access Current attributes
- Services don't accept request objects
- Controllers are thin (HTTP concerns only)
- Domain logic lives in models, not services
- Callbacks score 4+ or are extracted
- Concerns are behavioral, not code-slicing
- Abstractions don't span multiple layers
- Tests verify appropriate layer responsibilities
Guidelines
- Use domain language - Name models after business concepts (Participant, not User; Cloud, not GeneratedImage)
- Patterns before abstractions - Let code age before extracting; premature abstraction is worse than duplication
- Services as waiting room - Don't let
app/servicesbecome permanent residence for code - Explicit over implicit - Prefer explicit parameters over Current attributes
- Extraction thresholds - Consider extraction when methods exceed 15 lines or call external APIs