--- name: layered-rails description: Design Rails applications using layered architecture. Use when analyzing codebases for architecture violations, planning feature implementations, deciding where code belongs, or extracting abstractions from fat models/controllers. Complements dhh-coder (which keeps things simple) with guidance for when complexity demands structure. --- # Layered Rails Architecture **Audience:** Rails developers working on applications that have outgrown single-file patterns. **Goal:** Know which layer code belongs in, when to extract, and which existing skill handles the implementation. ## Four-Layer Architecture ``` Presentation → Application → Domain → Infrastructure (HTTP/UI) (Orchestration) (Business) (Persistence/APIs) ``` **Core rule:** Lower layers MUST NOT depend on higher layers. Data flows top-to-bottom only. ### Layer Responsibilities | Layer | Owns | Does NOT Own | |-------|------|-------------| | **Presentation** | HTTP concerns, params, rendering, channels, mailers | Business logic, direct DB queries | | **Application** | Orchestration across models, authorization, form validation | Persistence details, rendering | | **Domain** | Business rules, validations, associations, value objects | HTTP context, request objects, `Current.*` | | **Infrastructure** | ActiveRecord, external APIs, file storage, caching | Business rules, presentation | ### Common Layer Violations | Violation | Why It's Wrong | Fix | |-----------|---------------|-----| | `Current.user` in model | Domain depends on presentation context | Pass user as explicit parameter | | `request` param in service | Application depends on presentation | Extract needed values before calling service | | Pricing calc in controller | Business logic in presentation | Move to model method or service | | All logic in services, anemic models | Domain layer is hollow | Keep domain logic in models; services orchestrate | | Model sends emails directly | Domain depends on infrastructure side-effects | Use callbacks only for data transforms; extract delivery | ## The Specification Test Diagnostic for misplaced code: 1. List every responsibility the object handles 2. For each, ask: "Does this belong to this layer's primary concern?" 3. If NO → extract to the appropriate layer **Example:** A `User` model that handles authentication, avatar processing, notification preferences, and activity logging. - Authentication → Domain (keep) - Avatar processing → Infrastructure (extract to service/job) - Notification preferences → Domain (keep as concern) - Activity logging → Infrastructure (extract to observer/event) See [references/extraction-signals.md](references/extraction-signals.md) for the full methodology. ## Callback Scoring Rate each callback 1-5. Extract anything scoring 1-2. | Score | Type | Example | Action | |-------|------|---------|--------| | 5 | Transformer | `before_validation :normalize_email` | Keep | | 4 | Normalizer | `before_save :strip_whitespace` | Keep | | 4 | Utility | `after_create :update_counter_cache` | Keep | | 2 | Observer | `after_save :notify_admin` | Consider extracting | | 1 | Operation | `after_create :send_welcome_email, :provision_account` | Extract | **Rule of thumb:** If removing the callback would break the model's own data integrity → keep. If it triggers external side-effects → extract. ## Pattern Selection **"Where should this code go?"** | Situation | Pattern | Layer | Skill | |-----------|---------|-------|-------| | Complex multi-model form | Form Object | Presentation | — | | Request param filtering | Filter Object | Presentation | — | | View-specific formatting | Presenter / ViewComponent | Presentation | `viewcomponent-coder` | | Authorization rules | Policy Object | Application | `action-policy-coder` | | Business operation (one-time) | Service / Interaction | Application | `active-interaction-coder` | | Multi-model orchestration | Service Object | Application | `active-interaction-coder` | | State lifecycle management | State Machine | Domain | `aasm-coder` | | Complex reusable query | Query Object | Domain | — | | Immutable concept (Money, DateRange) | Value Object | Domain | — | | Shared model behavior | Concern | Domain | — | | Typed configuration | Config Object | Infrastructure | `anyway-config-coder` | | Domain events / audit trail | Event Sourcing | Infrastructure | `event-sourcing-coder` | | JSON-backed attributes | Store Model | Domain | `store-model-coder` | ### Decision Tree ``` Is it about HTTP/params/rendering? YES → Presentation layer Multi-model form? → Form Object Filtering params? → Filter Object Formatting for view? → Presenter or ViewComponent NO ↓ Is it authorization? YES → Policy Object (action-policy-coder) NO ↓ Does it orchestrate multiple models/services? YES → Application layer One-time operation? → Service/Interaction (active-interaction-coder) Needs typed inputs? → ActiveInteraction (active-interaction-coder) NO ↓ Is it a business rule about a single model? YES → Domain layer (keep in model or concern) Has state transitions? → AASM (aasm-coder) Reusable query? → Query Object Immutable value? → Value Object NO ↓ Is it about persistence/external APIs/caching? YES → Infrastructure layer ``` ## Services as Waiting Rooms `app/services/` is a **temporary staging area**, not a permanent home. - Services that survive should eventually reveal the real abstraction they represent - If a service wraps a single model operation → it probably belongs in the model - If a service coordinates 3+ models → it's a legitimate orchestrator - If a service grows complex → look for Form Object, Policy, or Query Object hiding inside **Smell test:** If `app/services/` has 50+ files and no subdirectories, the waiting room has become permanent storage. ## Extraction Signals When to extract code from existing locations: | Signal | Threshold | Action | |--------|-----------|--------| | Method length | > 15 lines | Extract method or object | | External API call in model | Any | Extract to service/gateway | | God object | High churn × high complexity | Decompose (see [references/extraction-signals.md](references/extraction-signals.md)) | | Spec exceeds layer concern | Specification test fails | Extract to appropriate layer | | Callback score | 1-2/5 | Extract to service or event handler | | Duplicated query logic | 2+ locations | Extract Query Object | | `Current.*` in model | Any usage | Pass as explicit parameter | See [references/extraction-signals.md](references/extraction-signals.md) for the complete methodology. ## Model Organization Recommended ordering within model files: ```ruby class Order < ApplicationRecord # 1. Extensions/DSL (has_secure_password, acts_as_*) # 2. Associations # 3. Enums # 4. Normalizations # 5. Validations # 6. Scopes # 7. Callbacks (transformers/normalizers only — score 4-5) # 8. Delegations # 9. Public methods # 10. Private methods end ``` ## When Layered vs DHH Style This skill complements `dhh-coder`, not replaces it. | Situation | Use | |-----------|-----| | Small/medium app, standard CRUD | `dhh-coder` — keep it simple | | Complex domain, multiple bounded contexts | `layered-rails` — add structure | | Authorization beyond simple checks | `action-policy-coder` via layered guidance | | Fat model with 500+ lines | `layered-rails` extraction signals | | Standard controller actions | `dhh-coder` — 7 REST actions | | Multi-step business operation | `active-interaction-coder` via layered guidance | **Default to simplicity.** Reach for layered patterns only when complexity demands it. ## Success Checklist - [ ] No reverse dependencies (lower layers don't reference higher) - [ ] Models don't access `Current` attributes - [ ] Services don't accept request/controller objects - [ ] Controllers contain only HTTP concerns - [ ] Domain logic lives in models, not leaked into services - [ ] All callbacks score 4+ (or extracted) - [ ] Concerns group by behavior, not by artifact type - [ ] Each abstraction belongs to exactly one layer ## Cross-References | Need | Skill | |------|-------| | Authorization policies | `action-policy-coder` | | Typed business operations | `active-interaction-coder` | | State machines | `aasm-coder` | | Operations + state routing | `business-logic-coder` | | ViewComponents | `viewcomponent-coder` | | Typed configuration | `anyway-config-coder` | | Event sourcing / audit | `event-sourcing-coder` | | JSON attributes | `store-model-coder` | | Refactoring execution | `rails-refactorer` | | DHH-style simplicity | `dhh-coder` | | Pattern catalog details | [references/pattern-catalog.md](references/pattern-catalog.md) | | Extraction methodology | [references/extraction-signals.md](references/extraction-signals.md) |