$npx -y skills add ThibautBaissac/rails_ai_agents --skill extraction-timingGuides decisions about when and how to extract code into services, queries, concerns, form objects, or other patterns. Use when deciding whether to extract code, choosing between patterns (service vs concern vs query), evaluating if a base class or abstraction is needed, or when
| 1 | You are an expert in Rails code organization and extraction decisions. Help decide **when** to extract, **what pattern** to use, and **when to keep it simple**. |
| 2 | |
| 3 | ## Core Philosophy: Skinny Everything |
| 4 | |
| 5 | The 2025 Rails consensus has evolved beyond "Fat Models" to **Skinny Everything**: |
| 6 | - **Controllers**: orchestrate (delegate to services, render responses) -- max ~10 lines per action |
| 7 | - **Models**: persist (validations, associations, scopes, simple predicates) -- max ~100 lines |
| 8 | - **Services**: contain business logic (multi-step operations, external calls, orchestration) |
| 9 | - **Views**: display markup with zero logic |
| 10 | |
| 11 | ## Extraction Thresholds |
| 12 | |
| 13 | | Signal | Action | |
| 14 | |--------|--------| |
| 15 | | Controller action exceeds ~10 lines of business logic | Extract to service object | |
| 16 | | Model exceeds ~100 lines | Extract business logic to services, complex queries to query objects | |
| 17 | | Query joins multiple tables or has conditional clauses | Extract to query object | |
| 18 | | Form touches multiple models or has custom validation | Extract to form object | |
| 19 | | Display formatting logic in model | Extract to presenter | |
| 20 | | UI element reused across 2+ views | Extract to ViewComponent | |
| 21 | | Shared behavior across 2+ models (narrow, simple) | Extract to concern | |
| 22 | | 5+ concrete implementations with identical structure | Extract base class | |
| 23 | | One-off operation | **Don't extract. Inline is fine.** | |
| 24 | |
| 25 | ## Decision Tree: "Where Should This Code Go?" |
| 26 | |
| 27 | ``` |
| 28 | Is it a database query? |
| 29 | ├── Simple (one table, one condition) → Model scope |
| 30 | └── Complex (joins, conditionals, reused) → Query object |
| 31 | |
| 32 | Is it business logic? |
| 33 | ├── Simple CRUD on one model → Controller inline (or model method) |
| 34 | ├── Multi-step operation → Service object |
| 35 | ├── Involves external API → Service object |
| 36 | └── Spans multiple models → Service object with transaction |
| 37 | |
| 38 | Is it shared behavior? |
| 39 | ├── Property of the model (soft-delete, slugs, search) → Concern |
| 40 | └── Operation on the model (checkout, import, sync) → Service object |
| 41 | |
| 42 | Is it display logic? |
| 43 | ├── Formatting one model's data → Presenter (SimpleDelegator) |
| 44 | ├── Reusable UI element → ViewComponent |
| 45 | └── Simple helper method → Keep in helper (use sparingly) |
| 46 | |
| 47 | Is it validation? |
| 48 | ├── Single model, standard rules → Model validation |
| 49 | ├── Multi-model form → Form object |
| 50 | └── Business rule (not data integrity) → Service validation |
| 51 | ``` |
| 52 | |
| 53 | ## Settled Debates |
| 54 | |
| 55 | ### Concerns vs Service Objects |
| 56 | |
| 57 | | | Concerns | Service Objects | |
| 58 | |--|---------|----------------| |
| 59 | | **Use for** | Simple shared model properties | Multi-step business operations | |
| 60 | | **Examples** | `SoftDeletable`, `Searchable`, `Sluggable` | `CreateOrder`, `ProcessRefund`, `ImportCsv` | |
| 61 | | **Max size** | ~30 lines | No hard limit (but SRP applies) | |
| 62 | | **Test via** | Including model's specs | Isolated unit specs | |
| 63 | |
| 64 | **Rule:** If the behavior is a *property* of the model, use a concern. If it's an *operation* on the model, use a service. |
| 65 | |
| 66 | ### STI vs Polymorphic Associations |
| 67 | |
| 68 | | | STI | Polymorphic | |
| 69 | |--|-----|------------| |
| 70 | | **Use when** | Subclasses share >80% of columns | Types have unique attributes | |
| 71 | | **Table** | One shared table with `type` column | Separate tables per type | |
| 72 | | **Avoid when** | >20% columns are NULL for some subtypes | Types are fundamentally similar | |
| 73 | |
| 74 | ### Callbacks vs Explicit Calls |
| 75 | |
| 76 | **Rule:** Callbacks for data normalization only. Everything else is explicit. |
| 77 | |
| 78 | | Acceptable Callbacks | Must Be Explicit (in services) | |
| 79 | |---------------------|-------------------------------| |
| 80 | | `before_validation :strip_whitespace` | Sending emails | |
| 81 | | `before_save :downcase_email` | Enqueuing background jobs | |
| 82 | | `before_destroy :check_dependencies` | Calling external APIs | |
| 83 | | `after_initialize :set_defaults` | Creating related records with business logic | |
| 84 | |
| 85 | ## Anti-Pattern Checklist |
| 86 | |
| 87 | Before extracting, verify you're not creating: |
| 88 | - [ ] A service that wraps a single `model.update!` call (Service Graveyard) |
| 89 | - [ ] A base class for only 2 services (Premature Abstraction) |
| 90 | - [ ] A concern with multiple responsibilities (Kitchen Sink Concern) |
| 91 | - [ ] A helper that should be a presenter or component |
| 92 | - [ ] An abstraction for a hypothetical future need (YAGNI violation) |
| 93 | |
| 94 | ## Reference |
| 95 | |
| 96 | See @docs/rails-development-principles.md for the complete development principles guide including SOLID, testing strategy, security, and performance. |