Ruby on Rails
Pathrule2 Rules • 5 Memories
Rails 8 changed the defaults that matter, and assistants trained on older Rails keep reaching for the old ones. Background jobs, caching, and WebSockets now run on the database through Solid Queue, Solid Cache, and Solid Cable instead of assuming Redis. Deploys go out with Kamal and Thruster, assets go through Propshaft, and the view layer is Hotwire rather than a JavaScript build. This bundle captures those defaults, plus the two things that break Rails apps at scale: N+1 queries and migrations that lock a table.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
Rules
2Declare associations you traverse and let strict loading catch the rest/app/modelshighstrictEvery collection that renders an association loads it with includes or preload, and models opt into strict_loading so a missed one fails in development.
| 1 | Active Record's lazy associations turn a clean partial into hundreds of queries. Rails gives you the tools to make that impossible rather than to notice it in production. |
| 2 | |
| 3 | - Load what the view renders: `includes` when you want Rails to pick the strategy, `preload` to force separate queries, `eager_load` to force one join (needed when you filter or order on the joined table). |
| 4 | - Turn misses into errors, not latency: `config.active_record.strict_loading_by_default = true`, or `strict_loading` on the specific association. A `StrictLoadingViolationError` in development is cheaper than a slow query log in production. |
| 5 | - Use `counter_cache` or `size` on a loaded association instead of `count`, which issues a query every call, and `exists?` instead of `present?` on an unloaded relation. |
| 6 | - Batch large reads with `in_batches` or `find_each` so one request never materialises the whole table, and select only the columns you use with `select` or `pluck`. |
| 7 | - Never query inside a view, helper, or serializer method that runs per row. Build a hash keyed by id in the controller and look it up. |
Memories
5The Solid stack is the default: database-backed queue, cache, and cable/configRails 8 runs jobs, cache, and WebSockets on the database through Solid Queue, Solid Cache, and Solid Cable, so a new app needs no Redis.
| 1 | Rails 8 shipped the Solid stack as the default adapters, which removes Redis from the dependency list for most applications. Adding it back is a decision that needs a reason. |
| 2 | |
| 3 | - Background jobs run on Solid Queue (an Active Job backend that stores jobs in the database), the cache store is Solid Cache, and Action Cable uses Solid Cable. All three are configured in the generated app and deploy with it. |
| 4 | - Keep the queue on its own database (the generated `queue` database) so job churn does not compete with application traffic for connections or vacuum. |
| 5 | - Redis is still the right answer for genuinely high-throughput queues and for cache workloads that need sub-millisecond reads at high concurrency. Below that, the operational win of one fewer service is larger than the latency difference. |
| 6 | - Solid Cache is disk-backed and designed for a much larger cache than a memory store, so cache more aggressively than Redis habits suggest, and set TTLs deliberately. |
| 7 | - SQLite is a supported production database in Rails 8 for single-server apps, and the Solid stack is what makes that viable. |
| 8 | |
| 9 | See /db for the migration rules the queue and cache tables also obey, and the background-jobs-queues pattern for queue design that is not Rails specific. |
Callbacks, concerns, and where a workflow actually belongs/app/modelsCallbacks are for keeping a record consistent; anything that touches another system belongs in an explicit object the caller can see.
| 1 | The Rails model is the easiest place to put behaviour and the hardest place to find it later. Draw the line by side effect. |
| 2 | |
| 3 | - A callback may normalise or derive a value on the record being saved. It should not send email, enqueue a job that depends on other systems, call an API, or update unrelated records. Those fire in tests, fixtures, seeds, and bulk operations you did not plan for. |
| 4 | - Multi-step workflows go in a plain command or service object called from the controller or job. It is readable at the call site, testable without a save, and reusable from a rake task. |
| 5 | - Reusable query vocabulary goes in scopes on the model or in a query object. Long chains rebuilt in three controllers are a scope waiting to be named. |
| 6 | - Concerns are for genuinely shared behaviour, not for splitting one large model into files. A concern that only one model includes is just indirection. |
| 7 | - Bulk paths (`update_all`, `insert_all`, `delete_all`) skip validations and callbacks by design. If a rule must always hold, put it in the database as a constraint. |
| 8 | |
| 9 | See /app for the controller rules and /db for the constraint that backs each validation. |
Hotwire first: Turbo Frames, Streams, and Stimulus for behaviour/app/viewsRails 8 renders HTML and updates it over the wire; reach for Turbo Frames and Streams before adding a client-side framework.
| 1 | The default Rails view layer is server-rendered HTML plus Hotwire. Most interactive requirements are solved without a JSON API or a build step. |
| 2 | |
| 3 | - Scope a partial update with a Turbo Frame: wrap the region, link or submit into it, and the response replaces just that frame. This covers inline edit, pagination, tabs, and modals. |
| 4 | - Push updates from the server with Turbo Streams (append, prepend, replace, remove), including from a background job over Action Cable. This is how you get live updates without polling. |
| 5 | - Add behaviour with a Stimulus controller attached to markup via data attributes. It survives Turbo navigation, which is exactly where ad hoc `DOMContentLoaded` scripts break. |
| 6 | - Assets go through Propshaft with import maps (or a bundler only if you genuinely need one). Sprockets and a Node build pipeline are legacy for a new Rails 8 app. |
| 7 | - Introduce a client-side framework only for genuinely stateful, offline-ish, or highly interactive surfaces, and keep it to that surface rather than the whole app. |
| 8 | |
| 9 | See /app for the controller rules that these views call into. |
Credentials and the Kamal deploy path/configSecrets live in encrypted credentials or the environment, and deploys run through Kamal with Thruster in front of the app container.
| 1 | Rails 8 generates a container-first deploy, and the parts fit together in a specific way. |
| 2 | |
| 3 | - Keep secrets in encrypted credentials (`rails credentials:edit`, per-environment files) or in environment variables injected at deploy. The master key never lands in the repo, and `config/master.key` stays gitignored. |
| 4 | - Read configuration through `Rails.application.config` or credentials, not by scattering `ENV[...]` calls through the code. One place to see what an environment must provide. |
| 5 | - Kamal deploys the app image to your own servers over SSH with zero-downtime container swaps; Thruster sits in front of Puma to handle TLS, HTTP/2, compression, and static file serving with X-Sendfile semantics. |
| 6 | - Run migrations as part of the release, restart job workers after a deploy so they pick up new code, and keep the health check endpoint fast and dependency-free so a slow database does not fail the deploy. |
| 7 | - Log in the container to stdout as structured lines, and let the platform ship them. Do not write log files inside an immutable image. |
| 8 | |
| 9 | See the docker-containers and secrets-env-management patterns for the parts that are not Rails specific. |
Migrations that do not take the site down/dbSchema changes are additive and multi-step, indexes are added concurrently, and destructive changes wait for a deploy that no longer needs the column.
| 1 | A migration is a deploy against a live database. On a table with millions of rows, the wrong one holds a lock long enough to time out every request. |
| 2 | |
| 3 | - Add columns nullable, backfill in batches, then add the constraint. Adding a NOT NULL column with a default rewrites the table on older engines and still needs care on new ones. |
| 4 | - Add and remove indexes concurrently on PostgreSQL (`add_index ..., algorithm: :concurrently`) and disable the wrapping transaction for that migration (`disable_ddl_transaction!`), because a concurrent index cannot run inside one. |
| 5 | - Removing a column is a two-deploy operation: stop referencing it (and add it to `ignored_columns` so Active Record stops selecting it), ship, then drop it. |
| 6 | - Backfill in a separate migration or a job with batching (`in_batches`), never in the schema migration itself, and make it re-runnable. |
| 7 | - Keep `db/schema.rb` as the source of truth of the current schema and regenerate it rather than hand-editing; resolve conflicts by re-running migrations, not by merging the file. |
| 8 | |
| 9 | See /app/models for the eager loading rule and the postgres-schema pattern for index and constraint design. |
Why this pattern
AI agents write Rails as if it were 2019: Redis and Sidekiq assumed for every queue, Sprockets for assets, a JavaScript framework in the view, and querysets that fire one query per row.
Built for Ruby teams on Rails 8 who deploy with Kamal and want the framework's own defaults.
Keeps your assistant from:
- Adding Redis and Sidekiq when Solid Queue and Solid Cache are the framework default
- Rendering a collection that lazy-loads an association once per row
- Writing a migration that adds an index or a NOT NULL column and locks a large table
- Reaching for a JavaScript framework where Turbo Frames and Streams already fit
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-08-24