Can you define Ruby on Rails and outline the key factors that contribute to its widespread use in modern web application development?
KnownRevise
Ruby on Rails is a server-side web application framework written in Ruby that follows the Model-View-Controller (MVC) architectural pattern. Its widespread use stems from its emphasis on developer productivity, integrated tooling, strong conventions, and a mature software ecosystem.
Read full answerHide full answer
Applies toRuby on Rails 8
Core Philosophy
Rails is organized around two complementary principles:
Convention over configuration: Sensible defaults define expected file locations, naming conventions, routing, and much of the application structure. Developers can therefore focus on application behavior instead of repeatedly configuring basic project decisions.
Don’t Repeat Yourself (DRY): Rails encourages developers to centralize behavior in reusable models, services, helpers, and components instead of duplicating it throughout controllers, views, and background jobs.
These principles reduce boilerplate while remaining flexible when an application genuinely needs a different structure.
Key Components and Ecosystem
Rails combines several full-stack components:
Active Record provides an object-relational mapper, migrations, validations, callbacks, and associations for relational databases.
Action Pack handles routing, controller processing, and the Rack integration underlying HTTP requests.
Action View supplies the template system used to render HTML and other view formats.
Active Job and Action Mailer define standard interfaces for background processing and email delivery.
RubyGems provides access to a large collection of packaged libraries, while migrations keep database schema changes versioned and reproducible when applied consistently across environments.
Why It Is Widely Used
Rapid application development: Integrated generators, conventions, and built-in components accelerate common web-development tasks.
Coherent full-stack design: The framework provides consistent patterns for models, controllers, views, jobs, mail, routing, and testing.
Large ecosystem: RubyGems and Rails-specific libraries cover authentication, payments, administration, APIs, monitoring, and many other domains.
Community and documentation: Rails has a long-established community, extensive official documentation, and widely available learning resources.
Deployment flexibility: Applications can run on supported servers, containers, and managed platforms, while many hosting providers offer Rails-specific deployment workflows.
Modern Rails Features
Rails 7 made Hotwire, including Turbo and Stimulus, the default front-end approach for newly generated applications, reducing the need to build every interaction around a separate JavaScript framework.
Rails 8 made Solid Queue, Solid Cache, and Solid Cable the default job backend, cache store, and Action Cable adapter for newly generated applications. This allows those services to operate without external infrastructure such as Redis by default.
Rails 8 also introduced a built-in authentication generator for scaffolding a basic password-based authentication flow. It provides a starting point rather than a complete user-management system.
Security and Best Practices
Use strong parameters to restrict the attributes a controller may assign, reducing the risk of unauthorized mass assignment.
Apply Active Record validations and database constraints to preserve required data invariants.
Never interpolate user input into SQL; use hash conditions or bound parameters.
Prevent N+1 queries with includes, preload, or eager_load when association data is needed.
Keep sensitive configuration in environment variables or a secrets manager rather than source code, and use HTTPS for production traffic.
Common Pitfalls
Failing to index frequently queried columns can cause performance to deteriorate as data grows. Placing substantial business logic in views or controllers makes it harder to test and reuse, while excessive Active Record callbacks can obscure control flow and create unexpected side effects.
Import maps are a JavaScript delivery mechanism, whereas esbuild is a bundler; applications can use importmap-rails or a bundler-based setup such as jsbundling-rails, depending on their asset requirements. Poorly planned asset delivery can increase page-load times.
Finally, database changes should be committed as migrations and applied with commands such as bin/rails db:migrate in development and deployment. This keeps schema changes tracked and reproducible, although teams still need disciplined deployment practices to avoid environmental schema drift.
2
What role does the Gemfile play in a Rails application’s dependency management?
KnownRevise
The Gemfile declares the Ruby gems a Rails application depends on, including the framework itself and libraries required by specific parts of the application. Bundler uses this file to resolve and install those dependencies into a consistent local environment.
Read full answerHide full answer
Applies toRuby on Rails 8
How It Works
A typical Rails application includes a Gemfile at its project root. The file uses Bundler’s Ruby DSL to declare:
Ruby version: The Ruby runtime required by the application.
Gem sources: The repositories from which gems may be downloaded.
Direct dependencies: Gems such as rails, pg, or redis.
Dependency groups: Separate sets of gems for development, testing, production, or other purposes.
Local gems: Application-specific gems exposed through the gemspec method.
Bundler reads the Gemfile, resolves compatible versions, and writes the selected versions and dependency relationships to Gemfile.lock.
Dependency Groups
Bundler groups organize dependencies into sets that can be installed or excluded independently. They are not automatically equivalent to deployment environments; exclusion behavior depends on Bundler configuration, such as the without setting.
Rails 8’s generated Gemfile commonly uses:
group :development, :test: For tools needed during both development and testing.
group :development: For tools needed only during development.
Ungrouped dependencies: Commonly used for dependencies required in every environment, including production.
A group :production can be declared when its contents should be managed as a separate Bundler scope, but it is not required for production dependencies.
Gemfile and Gemfile.lock
The two files serve different purposes:
File
Purpose
Gemfile
Declares the dependencies the application directly requires and the groups they belong to.
Gemfile.lock
Records the exact versions Bundler selected, along with transitive dependencies and platform-specific variants.
The Gemfile.lock should normally be committed to source control for applications. This ensures that developers, CI systems, and production deployments use the same dependency versions. It is regenerated when dependencies are intentionally changed rather than edited by hand.
Version Requirements
A gem declaration can use version requirements to express compatibility constraints. Broad requirements leave Bundler room to select an appropriate release, while exact pins provide stricter reproducibility. The lockfile supplies the exact installed version in practice.
For Rails applications, the rails gem determines the framework version, while other gems support features such as database access, background jobs, caching, or API serialization.
Best Practices
Keep dependency declarations in the Gemfile; avoid installing gems manually on individual machines.
Run bundle install to install dependencies and update the lockfile.
Run bundle exec for commands that must use the application’s bundled gems, such as bundle exec rails test.
Use dependency groups to keep environment-specific packages separate.
Review changes to Gemfile.lock, especially when updating Rails or major dependencies.
Remove unused gems instead of leaving them in the project.
Keep the Ruby version and Rails version aligned with the application’s deployment configuration.
3
How does the convention over configuration principle shape the design and behavior of a Ruby on Rails application?
KnownRevise
Convention over configuration means that Ruby on Rails provides sensible, widely accepted defaults so developers can focus on application behavior instead of repeatedly configuring framework mechanics. Explicit configuration remains appropriate when an application’s requirements differ from those defaults.
Read full answerHide full answer
Applies toRuby on Rails 8
How It Works
Rails standardizes naming, file placement, autoloading, generators, and integration among its components. For example, rails generate model Article title:string creates the conventional model, migration, test, and fixture files. The generated model inherits from ApplicationRecord, which inherits from ActiveRecord::Base.
Rails also uses established conventions to connect components:
Article conventionally maps to the articles table.
belongs_to :user conventionally uses the user_id foreign key.
ArticleController conventionally renders templates from app/views/article/.
resources :articles in config/routes.rb creates the standard RESTful routes.
These defaults reduce repetitive wiring while still allowing each relationship, route, and schema element to be customized when necessary.
Common Rails 8 Conventions
Application concern
Conventional Rails behavior
Routing
resources :articles explicitly creates standard RESTful routes
Models
Models inherit from ApplicationRecord, and Article maps to articles by default
Database columns
Migrations commonly add created_at and updated_at timestamp columns
Foreign keys
belongs_to :user conventionally uses user_id
Controllers
Controllers generally inherit from ApplicationController
Views
Templates follow controller and action paths under app/views/
Associations
Relationships are explicitly declared with methods such as belongs_to and has_many
Testing
Generated applications use Minitest and place tests under test/
Assets
New Rails 8 applications use Propshaft for asset handling
Background processing
Rails 8 introduced Solid Queue as the default Active Job backend for new applications; other adapters remain supported
Caching
Rails 8 introduced Solid Cache as the production cache-store default for new applications
Interactivity
Rails 7 made Hotwire the default front-end approach; Rails 8 applications include Turbo and Stimulus
These conventions make projects predictable: another Rails developer can usually locate a model, controller, route, or migration without extensive documentation.
What Rails Does Not Infer
Rails does not turn every class under app/models/ into an Active Record model or generate routes merely because matching controller classes exist. Those relationships require inheritance or explicit declarations.
Similarly, timestamp columns do not appear automatically in every table; a migration must create them. Validations and callbacks are not activated automatically: they execute when an application declares them on an Active Record model. Rails supplies the framework mechanisms, but developers still describe the application’s domain rules.
Advantages
Faster initial development: Generators and defaults handle repetitive setup.
Lower configuration volume: Developers configure only genuine deviations.
Predictable structure: Teams can navigate unfamiliar Rails applications more easily.
Integrated components: Active Record, Action Pack, Action View, and Active Job cooperate through shared conventions.
Easier onboarding: Developers can transfer Rails knowledge between applications.
Conventions are not universal: Specialized domains may require unusual names, mappings, or routes.
Implicit behavior can confuse newcomers: Developers must understand the relevant convention to diagnose behavior.
Exceptions accumulate: Repeated overrides can make an application harder to maintain.
Names still matter: A mismatched model filename, class name, template path, or action can cause loading, rendering, or routing errors.
Defaults do not replace judgment: Strong parameters, validations, authorization, CSRF protection, and output escaping still require correct application code.
A common interview mistake is describing the principle as “there is no configuration.” Rails supports extensive configuration; the principle says that developers should configure only what differs from established defaults.
Best Practices
Follow conventions unless a clear business or technical requirement justifies an exception. Use generators for conventional files, make deviations explicit and local, and document nonstandard mappings. Avoid changing framework-wide defaults for personal preference. Preserve Rails’ secure defaults by using strong parameters, Active Record validations, bound query parameters, CSRF protection, and framework-provided output escaping.
4
How would you define URL-to-controller mappings in a Rails application using the routing DSL?
KnownRevise
Rails uses the routing DSL in config/routes.rb to map incoming HTTP requests to controller actions. Routes can map a fixed path, a parameterized path, or a complete set of conventional RESTful endpoints.
Read full answerHide full answer
Applies toRuby on Rails 8
How Routes Work
The routing DSL is executed when the application starts. Each route specifies:
An HTTP verb such as get, post, patch, or delete
A URL path
The controller action that should handle matching requests
For example, get "about", to: "pages#about" sends GET /about to the about action of PagesController. The as: option creates a named route helper, so this route can be referenced as about_path.
Rails also creates route helpers for use in views, redirects, controllers, and JavaScript. Prefer these helpers over manually constructing URLs.
Defining Individual Routes
Common route forms include:
get "health", to: "health#show"
post "contact", to: "messages#create"
patch "profile", to: "profiles#update"
delete "sessions/:id", to: "sessions#destroy"
A colon marks a dynamic segment. In sessions/:id, the id segment becomes params[:id] in the controller.
The root shortcut defines the application’s root route:
root "home#index"
This maps GET / to HomeController#index and creates the root_path helper.
Defining RESTful Resource Routes
The resources method generates the conventional set of routes for a resource. For resources :articles, Rails creates:
HTTP method
Path
Controller action
Route helper
GET
/articles
index
articles_path
GET
/articles/new
new
new_article_path
POST
/articles
create
articles_path
GET
/articles/:id
show
article_path
GET
/articles/:id/edit
edit
edit_article_path
PATCH
/articles/:id
update
article_path
DELETE
/articles/:id
destroy
article_path
These routes map to ArticlesController. To generate only selected endpoints, use only or except:
resources :articles, only: [:index, :show]
Code Example
A typical config/routes.rb file might contain:
Rails.application.routes.draw do
root "home#index"
get "about", to: "pages#about", as: :about
resources :articles
end
The corresponding controllers would define actions such as:
class PagesController < ApplicationController
def about
end
end
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
Inspect the complete route table with bin/rails routes; filter it with bin/rails routes -g articles when needed.
Use named route helpers such as article_path(@article) instead of hard-coded paths.
Place more specific routes before routes that could capture them. Rails evaluates routes in declaration order.
Use constraints for formats or other constrained segments when required, but avoid unnecessary complexity.
Routes only determine where a request is sent. When reading submitted attributes in a controller, still use strong parameters and Active Record validations.
Keep authorization checks in controllers or policies; a route’s existence does not make an action publicly accessible.
5
What does CRUD mean, and how does Ruby on Rails use it to create and manage application resources?
KnownRevise
CRUD—Create, Read, Update, and Delete—describes the four basic operations an application performs on stored data. Rails implements these operations through conventional models, controllers, views, routes, and Active Record, with each HTTP request mapping to a controller action.
Read full answerHide full answer
Applies toRuby on Rails 8
How CRUD Maps to Rails
A Rails resource normally exposes the following actions:
CRUD operation
HTTP request
Rails action
Typical purpose
Create
POST /articles
create
Persist a new record
Read collection
GET /articles
index
List records
Read one
GET /articles/1
show
Display one record
Update
PATCH /articles/1
update
Modify an existing record
Delete
DELETE /articles/1
destroy
Remove an existing record
UI forms
GET
new, edit
Display forms for creating or updating
The resources :articles route declaration creates the standard RESTful routes for all these actions. Rails also supports PUT as an update method, although PATCH is normally preferred for partial updates.
Rails Conventions
Rails organizes CRUD functionality around several components:
Model: An Active Record class such as Article represents a database table. It provides methods such as new, save, find, update, and destroy.
Controller: ArticlesController receives requests, loads records, performs the operation, and chooses a response such as a redirect or rendered template.
Views: ERB templates render resource pages and forms. Rails scaffolds commonly generate index, show, new, edit, and _form templates.
Migration: A migration creates or changes the articles table and its columns.
Strong parameters: The controller explicitly permits the attributes a user may modify.
Validations and callbacks: These enforce application rules and run lifecycle operations before or after persistence.
A typical scaffold command such as bin/rails generate scaffold Article title:string body:text generates the model, migration, controller, routes, views, and tests. After running bin/rails db:migrate, the model can declare validations (for example, validates :title, presence: true) and callbacks, the controller uses a private article_params method with require and permit to whitelist attributes, and the views use form_with to build safe forms that submit to the appropriate RESTful route.
Best Practices
Use strong parameters for every create and update action; never assign permitted attributes directly from params.
Define Active Record validations for user-facing correctness and database constraints for critical invariants.
Use find when a missing record should produce a 404 Not Found response.
Render failed create or update requests with 422 Unprocessable Entity, allowing the form and validation errors to be displayed.
Keep authorization checks in the controller or an authorization layer before records are changed.
N+1 queries: If an index displays associated authors, preload the association with includes or preload rather than querying it for every article.
Unbounded collections: Article.all is fine for a small example, but production list pages should normally use pagination.
Overusing callbacks: Keep callbacks focused on cohesive lifecycle behavior; complex workflows are often clearer in service objects.
Incorrect error handling: Do not redirect after a failed validation, because the user would lose the submitted form and validation messages.
Direct SQL interpolation: Use Active Record or bound parameters instead of constructing SQL from user input.
6
What is the Rails way to make data available to a view after a controller action runs?
KnownRevise
In Rails, data is commonly made available to a view by assigning it to an instance variable in the controller action; the view can then access that variable when it is rendered. For reusable logic, controller methods can be exposed with helper_method, while explicit locals are useful for reusable partials.
Read full answerHide full answer
Applies toRails 8
How It Works
Controller actions: An action prepares the data needed by the view and assigns it to an instance variable, such as @articles.
View templates: ERB templates render in a view context that can access the controller’s instance variables, so @articles is available in the corresponding template.
Helper methods: helper_method exposes a controller method to views without duplicating its implementation in a helper module. The helper delegates the call back to the controller.
Local variables: Locals passed with render are available only to that rendering operation. They are especially useful for partials because they make a template’s required data explicit.
Non-HTML responses: JSON endpoints do not render an HTML view, so rendering JSON is not itself a controller-to-view data-transfer mechanism.
Choosing an Approach
Approach
Best use
Key consideration
Instance variables
Passing data from the main controller action to its view
Conventional Rails flow, but many instance variables can obscure dependencies
helper_method
Sharing a controller method with the view
Delegates the call to the controller; use helper modules for substantial reusable presentation logic
Explicit locals
Passing required data to a partial
Keeps reusable templates independent of controller state
View model or presenter
Packaging multiple values and presentation behavior
Useful when the view requires more structure than a single instance variable provides
Instance variables are the standard choice for the main action view. Explicit locals are usually preferable for partials because the partial declares exactly which values it expects.
Best Practices and Common Pitfalls
Keep data preparation in the controller; views should primarily handle presentation.
Avoid querying inside the view when the controller can load the required records beforehand.
Use includes, preload, or eager_load when displaying associated records to prevent N+1 queries.
Use helper_method for small shared methods, but move substantial reusable presentation logic into a helper module or dedicated presenter.
Pass explicit locals to reusable partials so their inputs are clear and they do not depend implicitly on controller state.
Use <%= %> for escaped output rather than raw HTML unless sanitization is intentional.
Do not expose sensitive attributes unnecessarily; retrieve or select only the data the template needs.
7
How does a request move through a Rails 8 controller action, including filters, rendering, and exception handling?
KnownRevise
The Rails 8 request lifecycle takes an HTTP request through the Rack middleware stack, routes it to a controller action, runs controller filters around the action, renders or redirects a response, and sends that response back through the middleware stack. Exceptions interrupt this flow and are handled either by the controller or by Rails’ exception-handling middleware.
Read full answerHide full answer
Applies toRuby on Rails 8
Request Flow
Rack middleware processes the request
Rails receives the request as a Rack environment.
Middleware can manage sessions, cookies, logging, compression, and other cross-cutting concerns.
CSRF verification is primarily performed by controller protection rather than by ordinary Rack middleware.
The router selects an endpoint
The router matches the HTTP method and path.
It resolves a route to a controller and action such as PostsController#show.
Rails instantiates the controller, assigns the request and response objects, and makes route and request parameters available through params.
Controller callbacks wrap the action
before_action callbacks run before the action.
around_action callbacks can execute setup, yield to the action, and perform cleanup.
The action method runs when the callback chain reaches it.
after_action callbacks run only when the chain completes without an exception or termination.
The action creates a response
An action can call render, redirect_to, or head.
If the action has not already produced a response, Rails normally performs implicit rendering by finding a template matching the action.
The resulting response includes its status, headers, body, cookies, and other metadata.
The response returns through the stack
The response travels back through the Rack middleware stack and is sent to the client.
Middleware can inspect or modify the response during this return path.
Controller Filters
Controller filters use Rails’ callback system:
before_action: Common for authentication, authorization, loading records, and validating required state.
around_action: Useful when work must surround the action, especially when setup and cleanup are paired.
after_action: Useful for auditing, analytics, and recording response information after successful processing.
skip_before_action: Exempts matching actions from a callback. Any security exemption should be narrow and replaced by an equivalent authorization check.
Before callbacks normally run in declaration order. Around callbacks nest according to their order, and an around callback must yield for the action to run. After callbacks normally run in reverse declaration order.
A callback can terminate the chain by rendering or redirecting. If an exception escapes the action, later callbacks do not normally run, but cleanup in an around_action’s ensure block still executes while the stack unwinds.
Rendering and Responses
Implicit rendering: Rails looks for the conventional template associated with the action and applies the applicable layout.
Explicit rendering: render can produce HTML, JSON, plain text, or another supported response body.
Content negotiation: respond_to selects a representation according to the requested format.
Redirection: redirect_to produces a 3xx response and does not also render the action’s template.
Empty responses: head sets the status and headers without a response body.
Duplicate rendering: Once an action has produced a response, attempting to render again can raise ActionController::DoubleRenderError.
The view is normally rendered in the context of the controller instance, so instance variables and helper methods are available to the template.
Exception Handling
A controller can handle selected exceptions with rescue_from. A handler can render an appropriate status and body, redirect the client, or translate the exception into a domain-specific response.
If no controller handler matches, the exception unwinds through the callback and middleware stacks. Rails’ exception-handling middleware then produces the final response. Development environments normally show detailed diagnostics, while production environments should return controlled error pages or API responses without exposing internal details.
Exception handling does not restore normal callback execution: an after_action is skipped when an exception occurs, although an around_action’s ensure block can release resources or perform cleanup.
Best Practices and Common Pitfalls
Keep business rules in models or service objects rather than crowding controller actions with unrelated logic.
Use strong parameters for user-submitted attributes.
Use includes, preload, or eager_load when associations are needed, preventing N+1 queries.
Prefer implicit rendering for conventional responses and explicit rendering when a different representation is required.
Use around_action for genuine setup-and-cleanup pairs rather than as a replacement for simple callbacks.
Do not rely on after_action for essential cleanup when an action may raise; use ensure or another failure-safe mechanism.
Never exclude an action from authentication or authorization without enforcing an equivalent check in that action.
8
What are strong parameters in Ruby on Rails, and what security risks do they mitigate when handling user input?
KnownRevise
Strong parameters are a Rails security mechanism that enforces explicit whitelisting of request attributes before they are passed to Active Record models for mass assignment. By requiring controllers to declare exactly which fields may be used, they prevent attackers from injecting unexpected parameters into form submissions and modifying sensitive database columns. This controller-layer guard is a core defense whenever user input creates or updates records.
Read full answerHide full answer
Applies toRails 8 (strong parameters introduced in Rails 4)
How It Works
When a request arrives, the params hash contains route parameters, query-string values, and form or JSON body fields. Strong parameters provide two chained methods: - require(:user) verifies that a top-level user key exists and raises ActionController::ParameterMissing if it does not. - permit(:name, :email) filters the nested hash, returning an ActionController::Parameters object marked as permitted.
Active Record mass-assignment methods such as create, update, and update! accept permitted parameters. If unpermitted parameters are passed, Active Record raises ActiveModel::ForbiddenAttributesError before any database write. Note that update_attributes and update_attributes! are deprecated since Rails 6.1; use update and update! instead.
Security Risks Mitigated
Without whitelisting, a malicious user can add hidden fields with browser developer tools or a custom HTTP client. If a model blindly accepts all parameters, an attacker could set admin: true, reassign owner_id, or change status and role values. This mass assignment vulnerability can lead to privilege escalation, data tampering, and authorization bypass. Strong parameters act as a firewall at the controller layer, so only intended attributes reach the model even if view-level protections are bypassed. The same protection applies to Hotwire/Turbo submissions, because the underlying parameter structure is unchanged.
Key Components
require: Validates presence of a key, usually the model name wrapping attributes.
permit: Defines an allow-list of scalar attributes; all others are rejected silently and logged in development.
Nested attributes: Associations are permitted with hashes, e.g. permit(:name, posts_attributes: [:title, :body]).
Arrays and scalars: Array-valued fields need explicit structure, e.g. permit(:name, tags: []).
permit!: Marks every key under the required parameter as permitted; it defeats the security goal and should be avoided.
Best Practices and Pitfalls
Always define parameter methods as private so they cannot be exposed as controller actions. Call require before permit, and never rely on view helpers or hidden fields to protect sensitive columns. Avoid permit! except in trusted internal APIs, and test parameter filters in request specs to confirm forbidden attributes are rejected and missing keys raise ActionController::ParameterMissing. In Rails 8, strong parameters remain the standard input-handling mechanism regardless of front-end technology.
9
How do member routes and collection routes differ in Ruby on Rails resourceful routing configurations?
KnownRevise
In Ruby on Rails routing, member routes target a specific resource instance identified by an ID, whereas collection routes operate on the entire set of resources without requiring an identifier. This distinction dictates the generated URL structure, the required parameters, and the intended semantic action within the controller.
Read full answerHide full answer
Applies toRails 8
Route Definition and Scope
When defining resourceful routes in config/routes.rb, the resources method generates the standard RESTful routes such as index, show, and create. Custom actions are added by nesting them inside member or collection blocks. A member block signals that the route applies to one record and includes the resource identifier in the path. A collection block signals that the route applies to the whole set and omits the identifier. By default the placeholder is :id; if a resource declares param: :slug, member routes use :slug instead.
URL Patterns and Helper Names
The path and route helper differ in both shape and plurality. For a Post resource, a member route such as post 'vote' inside member produces /posts/:id/vote and is reached with the singular helper vote_post_path(@post). A collection route such as get 'search' inside collection produces /posts/search and is reached with the plural helper search_posts_path. Member routes are commonly used for state changes on one object, such as approving, voting, or canceling. Collection routes are commonly used for queries, aggregations, searches, or bulk operations.
Controller Behavior
In the controller, member actions normally retrieve the specific record using params[:id], often through a before_action callback such as set_post. Collection actions do not receive an identifier and usually operate on query parameters such as params[:query]. This separation keeps instance-level logic from being accidentally applied to the whole dataset and keeps class-level or search logic from requiring a nonexistent ID.
Comparison
Aspect
Member route
Collection route
Scope
One resource identified by :id
The entire collection
Example path
/posts/:id/vote
/posts/search
Route helper
vote_post_path(@post) (singular)
search_posts_path (plural)
Identifier required
Yes
No
Typical actions
Vote, approve, cancel, retry
Search, export, bulk update, dashboard
Best Practices and Pitfalls
Prefer standard RESTful actions when the behavior fits index, show, create, update, or destroy; reserve custom member and collection routes for genuinely non-standard actions.
Keep IDs out of collection routes. Placing logic that needs params[:id] in a collection action causes missing-parameter errors and confusing URLs.
Do not force collection-wide operations into member routes. That complicates URL generation and suggests the wrong scope.
Use strong parameters for any mass assignment triggered by custom actions, and verify authorization on the specific member resource when applicable.
Limit generated routes with only: or except: on resources so custom member and collection routes do not multiply unnecessary endpoints.
Remember the param: option. When a resource uses a slug or UUID instead of :id, member routes adapt automatically, but collection routes remain unchanged.
10
What mechanisms does Ruby on Rails provide for persisting user session state across HTTP requests?
KnownRevise
Rails persists state across HTTP requests through session stores. The default keeps the serialized session in a signed client-side cookie, while alternative stores keep session data on the server and use the cookie only to identify it.
Read full answerHide full answer
Applies toRuby on Rails 8
Default Cookie Store
ActionDispatch::Session::CookieStore is Rails’ default session store. Rails serializes the session, signs it with the application’s secret_key_base, and sends it in a cookie with each request. Signing prevents clients from modifying the payload, but it does not provide confidentiality: the serialized value is encoded and can be decoded by the client.
This design requires no server-side session lookup, making it fast and horizontally scalable. Its main limitation is cookie size: browsers generally support approximately 4 KB per cookie, including its name and attributes. Consequently, cookie sessions should contain only small values such as user_id, not complete user records or large collections.
Server-Side Alternatives
Store
Where session data lives
Important behavior
ActionDispatch::Session::CookieStore
Signed cookie on the client
Stateless server; practical cookie-size limit
ActionDispatch::Session::CacheStore
Configured Rails.cache backend
Cookie contains a session ID; cache eviction or expiry ends the session
ActionDispatch::Session::ActiveRecordStore
Database rows
Server-side persistence and expiry; requires the activerecord-session_store gem
CacheStore uses whichever cache backend is configured for the application. Rails 8 generated applications use Solid Cache as the production cache backend by default; Redis or MemCached can instead be configured as cache backends. Because the session depends on cache availability, eviction, restart, or expiration can log users out.
ActiveRecordStore was extracted from Rails core in Rails 4 and is now provided by the activerecord-session_store gem. It is appropriate when sessions must be persisted in a relational database, but requires database cleanup and capacity planning for expired sessions.
Security and Expiration
The secret_key_base is the secret used to protect signed session data. Rails 8 applications commonly supply it through encrypted credentials or an environment variable in production. If it is compromised, attackers may be able to forge sessions. Rotating it can invalidate existing sessions, so rotations should be coordinated rather than performed as an uncoordinated routine toggle.
Production sessions should use HTTPS and cookies with the Secure, HttpOnly, and appropriate SameSite attributes. Secure prevents transmission over plain HTTP, HttpOnly prevents browser scripts from reading the cookie, and SameSite helps mitigate cross-site request abuse. SameSite does not replace Rails’ CSRF protection.
Because cookie-store payloads are signed rather than confidential, developers should not place passwords, access tokens, or unnecessary personal information in the session. Cookie, cache, and database stores can each have different expiry behavior, so expiration should be configured deliberately.
Using Sessions in Controllers
Controllers access sessions through the hash-like session object. Assigning a value makes it available to later requests, and deleting a key removes only that value. The session is not inherently tied to a particular browser or user; its scope depends on the cookie and the selected store.
On logout or a privilege change, reset_session discards the current session contents and starts a fresh session. This is especially important with server-side stores because it replaces the old server-side session state. By contrast, deleting only session[:user_id] logs the user out at the application level but leaves the rest of the session intact and does not itself constitute session-fixation protection.
Best Practices and Common Pitfalls
Store identifiers: Save user_id and reload current user data from the database when needed; this avoids stale records and oversized sessions.
Use reset_session for a full logout: Use it when all session state must be discarded, including state associated with an earlier privilege level.
Do not depend on session lifetime alone: Applications still need appropriate authentication, authorization, CSRF, and secure-cookie controls.
Plan cache eviction: With CacheStore, a cache eviction can terminate an otherwise valid session.
Do not store secrets in cookie sessions: A signed cookie protects integrity, not confidentiality.
11
How does the flash mechanism function within Ruby on Rails, and what is the correct way to set and display flash messages across requests?
KnownRevise
The flash is Rails’ request-spanning message store for short-lived notices and alerts. Assigning flash[:notice] makes a value available in the current request and the next request, while flash.now[:notice] limits it to the current request.
Read full answerHide full answer
Applies toRuby on Rails 8
How It Works
Because HTTP requests are independent, Rails stores flash data in the session. With a cookie-backed session store, the session is serialized into the browser’s cookie; with a server-side session store, it remains on the server.
By default, a value assigned with flash[:key] is available during the current request and remains available for the next request. Rails then removes it automatically after that next request. This lifecycle is designed for messages shown after a redirect.
Setting and Displaying Flash Messages
Assignment
Availability
Typical use
flash[:notice] = "Saved"
Current request and next request
Before redirect_to
flash.now[:notice] = "Invalid"
Current request only
Before render
redirect_to posts_path, notice: "Saved"
Sets :notice and redirects
Rails shorthand for setting flash before redirecting
Controllers and views can access flash data through the flash helper. A layout can iterate over all entries with flash.each and render each message.
Code Example
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def create
# Post is assumed to have a title column and a presence validation.
@post = Post.new(post_params)
if @post.save
# The notice is available during the next request.
redirect_to @post, notice: "Post was successfully created."
else
# The alert is available only in the response rendered now.
flash.now[:alert] = "Post could not be saved."
render :new, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title)
end
end
<!-- app/views/layouts/application.html.erb -->
<% flash.each do |type, message| %>
<%= tag.div(message, class: "flash #{type}") %>
<% end %>
Best Practices and Common Pitfalls
Use the correct lifetime: Use ordinary flash before a redirect and flash.now before rendering in the same request.
Control retention explicitly: Call flash.keep(:notice) while handling a request if that message must survive one additional request. Use flash.discard(:notice) to remove an unwanted entry or flash.clear to clear the current flash entries.
Do not confuse rendering with persistence: flash[:alert] set before render appears in that rendered response and, unless discarded, remains available for the next request.
Avoid sensitive data: Do not place passwords, access tokens, or private records in flash data because flash data is stored in the session.
Preserve output escaping: Rails helpers escape ordinary strings, but content explicitly marked html_safe is not escaped. Never mark untrusted flash content as safe.
Use consistent keys and styling: Common keys are :notice for informational messages and :alert for errors; map known keys consistently to CSS classes.
Keep messages lightweight: Flash data does not cause N+1 queries, but avoid loading database records merely to construct a message; use short text or identifiers instead.
12
In Ruby on Rails, what is Active Record, and how does it map application objects to database records and persist data?
KnownRevise
Active Record is Ruby on Rails’ object-relational mapping layer. It represents database rows as Ruby objects and automatically generates parameterized SQL to load, create, update, and delete records while enforcing validations, callbacks, and transactional integrity.
Read full answerHide full answer
Applies toRuby on Rails 8 (Active Record 8.0)
How It Works
Active Record implements the Active Record pattern: a model class maps to a database table, and each instance maps to one row. A User class conventionally maps to a users table, with the instance’s attributes reflecting column values. Rails assumes pluralized snake_case table names, an id primary key, and created_at/updated_at timestamps, though these can be overridden. ApplicationRecord is the usual base class for models. Ruby method calls such as User.find(3) or user.save are translated into parameterized statements rather than interpolated SQL.
Core Responsibilities
Model mapping: binds classes to tables and instances to rows.
Persistence: handles INSERT, UPDATE, and DELETE operations.
Query interface: exposes methods such as where, find, order, limit, and pluck, returning lazy relations that execute only when results are needed.
Schema management: integrates with migrations and schema dumping.
Connection management: uses a pooled connection per request or operation.
Data integrity: provides validations, callbacks, transactions, and optimistic locking.
For example, User.where(active: true).order(created_at: :desc) builds a relation; the query runs when the relation is enumerated or converted with to_a.
Persistence Lifecycle
A new record receives an INSERT when save is called; a persisted, changed record receives an UPDATE. Active Record tracks dirty attributes and updates only changed columns by default. Before persistence, validations run; an invalid record is not saved and save returns false, with messages available through errors.
Callbacks surround lifecycle events such as validation, creation, update, and deletion. Because callbacks run inside the database transaction, external side effects should usually be deferred to after_commit, which fires only after the transaction commits.
Transactions wrap multiple operations atomically. User.transaction rolls back all changes if an exception occurs. The bang variants save! and create! raise ActiveRecord::RecordInvalid on failure, which is useful when invalid data should halt execution.
Associations and Loading Data
Relationships are declared with belongs_to, has_many, has_one, and has_many :through. Associations enable navigation and can enforce referential integrity and dependent deletion.
Because associations are not always loaded automatically, iterating over them naively can trigger an N+1 query problem: one query for the parent collection and one query per associated record. Rails provides eager-loading strategies:
Method
Strategy
When to use
includes
Separate queries, or a join when filtered conditions require it
General-purpose eager loading; safest default
preload
Separate queries for each association
When associations are large and joined results would duplicate rows
eager_load
Single query using a LEFT OUTER JOIN
When filtering or sorting by columns on associated tables
Choosing the right method avoids redundant queries and keeps result sets correct.
Best Practices and Common Pitfalls
Use hash conditions or bound parameters: Prefer where(name: params[:name]) over string SQL; Active Record binds values safely.
Use strong parameters: Permit only expected attributes in controllers with require and permit.
Validate at the model boundary: Validations protect records regardless of the controller, job, or console code that creates them.
Avoid hidden N+1 queries: Use includes, preload, or eager_load, and check query counts in tests.
Do not default to update_column: It skips validations and callbacks; use update when those safeguards matter.
Keep transactions short: Holding a pooled connection during slow external work reduces throughput.
Treat callbacks carefully: Prevent recursion, excessive queries, and side effects that should wait until commit.
Remember database constraints still matter: Foreign keys, unique indexes, NOT NULL, and check constraints enforce integrity even if model validations are bypassed.
13
What is the process for adding a new Active Record model to a Rails application?
KnownRevise
Use the Rails model generator to create the model, migration, test, and fixture, then add validations and callbacks and run the migration. The generated model inherits from ApplicationRecord, which holds the shared configuration for Active Record models.
Read full answerHide full answer
Applies toRuby on Rails 8
What the Generator Does
The generate model command uses the model name and attribute list to create conventionally named files:
bin/rails generate model Article title:string body:text
For an Article model, it typically creates:
File
Purpose
app/models/article.rb
Defines the Article Active Record model
db/migrate/<timestamp>_create_articles.rb
Creates the articles table
test/models/article_test.rb
Contains model tests
test/fixtures/articles.yml
Provides test fixture data
By convention, Article maps to the articles table. ApplicationRecord is created when you run rails new; the model generator does not create it, so it must already exist for the generated model to inherit from it.
Define Schema and Behavior
Review the generated migration before applying it. Add database constraints for data integrity and model validations for application-level rules:
class CreateArticles < ActiveRecord::Migration[8.0]
def change
create_table :articles do |t|
t.string :title, null: false, limit: 200
t.text :body
t.timestamps
end
end
end
The model can then define validation and lifecycle behavior:
class Article < ApplicationRecord
validates :title, presence: true, length: { maximum: 200 }
before_validation :normalize_title
private
def normalize_title
self.title = title.to_s.strip
end
end
null: false enforces the rule at the database level, while validates produces user-friendly errors and prevents invalid records from being saved.
Persist and Use the Model
Run the migration to create or update the database schema:
bin/rails db:migrate
Create a record using create! when invalid data should raise an exception:
Use create/save when you want to inspect validation failures without an exception:
article = Article.new(title: "", body: "Missing a title")
if article.save
# The record was saved.
else
article.errors.full_messages
end
Code Example
The complete model-generation workflow is:
bin/rails generate model Article title:string body:text
After updating the generated migration and model as shown above, apply the schema:
bin/rails db:migrate
Then exercise the model through the Rails console:
bin/rails console
article = Article.create!(
title: "Active Record models",
body: "Models map application objects to database tables."
)
Article.find(article.id)
Best Practices and Common Pitfalls
Use migrations: Change database structure through migrations rather than manually modifying the database.
Use strong parameters: When populating a model from a request, permit only expected attributes, e.g. params.require(:article).permit(:title, :body).
Keep invariants in the model: Put validation rules in the model so they apply consistently across controllers, console commands, jobs, and tests.
Use callbacks carefully: Callbacks are suitable for normalization and similar model lifecycle behavior, but they should not hide important external side effects.
Prevent N+1 queries: If Article belongs to an associated record, preload it with includes or preload when loading collections.
Test valid and invalid cases: Verify that valid records are saved and that invalid records produce the expected errors.
Review generated files: The generator supplies a starting point; it does not infer every validation, index, constraint, or association required by your domain.
14
How do database migrations work in Rails, and what purpose do they serve in an application?
KnownRevise
Migrations are versioned Ruby files that describe changes to a Rails application’s database schema, such as creating tables, adding columns, or defining indexes. They allow a team to evolve the database consistently across development, test, staging, and production environments.
Read full answerHide full answer
Applies toRails 8.0 with Active Record 8.0
How Migrations Work
Rails stores each migration in db/migrate using a timestamp-based filename, for example db/migrate/20250101000000_create_users.rb. The timestamp determines the order in which migrations run. Each migration inherits from ActiveRecord::Migration and defines the changes it should perform.
A migration can use change when its operations are automatically reversible:
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email, null: false
t.timestamps
end
add_index :users, :email, unique: true
end
end
For operations that cannot be reversed automatically, or when explicit reversal is preferable, define up and down methods instead:
class AddLegacyCodeToUsers < ActiveRecord::Migration[8.0]
def up
add_column :users, :legacy_code, :string
end
def down
remove_column :users, :legacy_code
end
end
Rails records successfully applied migrations in the schema_migrations table, allowing it to determine which migrations still need to run.
What Migrations Are Used For
Migrations are used for changes such as:
Creating or removing tables
Adding, removing, or renaming columns
Changing column types or constraints
Adding indexes for performance
Adding foreign keys to enforce relationships
Adding database-level defaults or nullability rules
Performing controlled data transformations when schema changes require them
Migrations affect the database directly, whereas Active Record models represent those tables in Ruby. Database constraints and model validations solve related but different problems. For example, a unique database index prevents duplicate values even when multiple application servers write concurrently; an Active Record uniqueness validation provides a more convenient application-level error.
db:migrate applies pending migrations and updates the schema representation, usually db/schema.rb. If the application is configured to use db/structure.sql, that file is generated instead. db:rollback reverts the most recently applied migration; use db:rollback STEP=3 to revert three migrations. db:prepare prepares a new database by creating it when necessary and bringing it to the current migration state.
In Rails deployments, migrations are normally run as part of the release process, before or alongside starting the new application version.
Best Practices and Common Pitfalls
Keep migrations focused: Prefer small, purpose-specific migrations that are easy to review and deploy.
Do not edit deployed migrations: Once a migration has reached shared or production environments, correct it with a new migration instead.
Use appropriate column definitions: Specify types, nullability, defaults, and indexes deliberately rather than relying on implicit behavior.
Avoid irreversible operations without a plan: Data deletion or destructive transformations should be separated from the schema migration and carefully reviewed.
Do not use migrations for request validation: Strong parameters and model validations belong in the application layer, while database constraints provide final enforcement.
Test migrations: Run them against a clean test database and verify both the forward migration and its rollback behavior when reversal is expected.
Avoid raw SQL with interpolated input: Use migration APIs or safely bound values; never construct SQL from untrusted user input.
Coordinate deployments: A migration that requires application code must be compatible with both the old and new application versions during a rolling deployment.
15
How do validations work in Rails 8 models, and why are they important for maintaining reliable application data?
KnownRevise
In Rails 8, Active Record validations are checks that run before records are created or updated, helping prevent invalid data from being persisted. They protect data integrity, produce useful user-facing errors, and keep business rules inside the model layer.
Read full answerHide full answer
Applies toRails 8
How Validations Work
A model declares validations using helper methods such as validates, validates_presence_of, validates_uniqueness_of, and validates_numericality_of. Rails attaches these checks to the model’s validation lifecycle.
When an application calls save, create, or update, Active Record normally runs validations before persistence:
If the record is invalid, persistence stops. save and update return false, while create returns an unsaved record.
Validation errors are available through record.errors.
If the record is valid, Rails persists it as part of the normal save process.
The bang methods save!, update!, and create! raise ActiveRecord::RecordInvalid when validation fails.
Calling valid? checks the record without writing it to the database. This is useful for forms and other operations that need to validate data before attempting a write.
Types of Validation
Rails provides built-in helpers for common data-integrity requirements:
Presence: Ensures required attributes are not blank.
Length: Restricts the size of a string, array, or other object responding to length.
Format: Checks a value against a regular expression.
Inclusion and exclusion: Restricts a value to an allowed list or rejects values in a prohibited list.
Numericality: Enforces numeric values and optional range constraints.
Uniqueness: Checks that a value is not already used by another record, optionally within a defined scope.
Comparison: validates_comparison_of, introduced in Rails 7.1 and available in Rails 8, checks comparisons such as greater than, less than, or equal to.
Most validators accept if and unless options for conditional execution. The on: :create and on: :update options restrict a validation to the corresponding persistence context.
For a custom validation method, register it with validate and add failures with errors.add:
validate :custom_rule runs a method on the model.
validates_with registers a reusable ActiveModel::Validator object.
Cross-attribute requirements can be implemented in a custom validation method or with an appropriate comparison validator.
Why They Are Important
Validations provide several benefits:
Data integrity: Invalid values are rejected before they reach persistent storage.
Consistent business rules: A rule such as “orders must have a positive total” is enforced wherever the model is used.
Better user feedback: Controllers and forms can display validation messages without persisting an invalid record.
Model-level encapsulation: The rule belongs with the data object instead of being duplicated across controllers, API endpoints, and background jobs.
Safer automation: Jobs and service objects that use normal Active Record persistence methods receive the same validation behavior as web requests.
Validation is especially useful for protecting against accidental omissions, invalid formats, inconsistent relationships, and values that violate domain-specific rules.
Validation and Database Constraints
Validations improve application behavior, but they do not completely replace database constraints. Database constraints remain necessary for protection against concurrent writes and failures occurring outside the normal Rails validation path.
Mechanism
Primary purpose
Rails validation
Provides application-level checks and readable error messages
Database constraint
Guarantees data integrity at the storage boundary
For example, a validates_uniqueness_of check can produce a friendly error, but two requests can pass that check simultaneously. A database unique index on the same column or combination of columns closes this race condition.
The same principle applies to foreign keys, NOT NULL constraints, and check constraints where supported by the database.
Best Practices and Common Pitfalls
Keep validation rules in the model, while reserving database constraints for guarantees that must survive concurrency and failures.
Use errors.add and errors.full_messages to communicate validation problems clearly.
Normalize values in a before_validation callback when appropriate, such as trimming whitespace before checking presence.
Do not rely on validation to escape output or sanitize input; output encoding, parameterized queries, and other security controls are still required.
Avoid save(validate: false) unless bypassing validation is explicitly necessary and the data has already been validated by another trusted mechanism.
Remember that methods such as update_column, update_columns, and update_all bypass model validations and callbacks.
Remember that uniqueness validation is a convenience check, not a substitute for a unique database index.
16
How would you define relationships between Rails models using association macros such as `has_many` and `belongs_to`?
KnownRevise
Rails associations connect model classes and let you navigate related records while enabling Active Record to manage foreign-key relationships. The association macros belong in the model, while foreign-key columns and database constraints are normally defined in migrations.
Read full answerHide full answer
Applies toRuby on Rails 8
How Associations Work
belongs_to: Declares that each child record belongs to one parent record. For example, every Post belongs to one User.
has_many: Declares that one parent record can have multiple child records. For example, one User can have many Post records.
Foreign key: A belongs_to association conventionally uses a column named after the associated model, such as user_id on the posts table.
Association methods: Defining the macros creates instance methods such as post.user, user.posts, and user.posts.build.
The association declaration itself does not create the database column or constraint. Use a migration for those database-level definitions.
Defining Associations
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true
end
With Rails 8’s generated defaults, belongs_to is required by default at the validation level. This default was introduced by Rails 5.0’s config.load_defaults 5.0; applications can override it with config.active_record.belongs_to_required_by_default = false. If the foreign key is intentionally nullable, declare the association explicitly as optional:
class Comment < ApplicationRecord
belongs_to :post, optional: true
end
The corresponding migration should define the relationship and enforce the database constraint:
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts do |t|
t.references :user, null: false, foreign_key: true
t.string :title, null: false
t.text :body
t.timestamps
end
end
end
t.references :user creates the user_id column and an index. The foreign_key: true option adds a database foreign-key constraint, preventing a post from referencing a nonexistent user.
Common Association Patterns
One-to-many: User has_many :posts and Post belongs_to :user.
One-to-one: Use has_one and belongs_to, usually with a unique foreign-key constraint:
class User < ApplicationRecord
has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
belongs_to :user
end
Many-to-many: Use a join model with two belongs_to associations:
class Student < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :courses, through: :enrollments
end
class Course < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :students, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
end
Polymorphic associations: Use when a record can belong to different types of parents:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
The commentable side then uses has_many :comments, as: :commentable.
Querying Related Records
Association methods can be used to create related records:
post = user.posts.create!(title: "Rails associations")
# Or build an unsaved record:
post = user.posts.build(title: "Unsaved post")
post.user # The user is already assigned
Be careful with large collections. Avoid loading every associated record into memory when only a subset is needed:
users = User.includes(:posts).limit(100)
Use includes or preload to prevent N+1 queries when accessing associations across many parent records. Use eager_load when you need SQL conditions against the joined table.
Code Example
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true
end
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts do |t|
t.references :user, null: false, foreign_key: true
t.string :title, null: false
t.text :body
t.timestamps
end
end
end
Best Practices and Pitfalls
Define foreign keys and indexes in migrations, not only in model associations.
Add dependent: :destroy or another deliberate deletion strategy when child records should not outlive their parent. Choose :destroy carefully for large collections; :delete_all bypasses callbacks but is faster.
Keep belongs_to validations consistent with the database definition. Use optional: true only when the relationship is genuinely optional.
Use has_many through for multi-table relationships instead of duplicating association logic.
Avoid calling user.posts.create! with an untrusted user_id; derive the parent from the current authenticated user.
When exposing associated records through a controller, permit only the attributes the application needs and assign the parent association explicitly:
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post
else
render :new, status: :unprocessable_content
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
17
What are Active Record model callbacks and how do they function during the lifecycle of an object?
KnownRevise
Active Record callbacks are hooks into the life cycle of an Active Record object that let you trigger logic before or after a state change. They automate behaviors such as data normalization, auditing, or enqueuing background jobs directly from the model, keeping controllers thin. Each callback is tied to a specific persistence event—create, update, destroy, or validation—and runs in a defined order within the surrounding database transaction.
Read full answerHide full answer
Applies toRails 8
Lifecycle Overview
Callbacks are grouped by the event they surround. The order matters because each step runs inside the same transaction until after_commit:
around_* callbacks wrap the remaining chain and must yield to continue. after_commit and after_rollback run outside the transaction, so after_commit is the safe place for emails, background jobs, or cache writes that should only happen after the record is truly persisted.
Declaring Callbacks
A callback can be declared as a symbol referencing a private or public method, an inline proc, or any callable object. Use symbol methods for anything beyond a one-line transformation because they are easier to test and keep the model readable. Conditions are added with if: and unless:, which accept symbols, procs, or arrays:
Conditions are evaluated at the time the callback runs, so they can inspect current attribute values.
Halting Execution
Since Rails 5, returning false from a callback no longer stops the callback chain. To halt persistence you must explicitly throw :abort. A before_save or before_create callback that throws :abort prevents the record from being saved and skips later callbacks in the same chain. This avoids accidental halts from values that happen to be falsey and makes the intent explicit.
Best Practices and Pitfalls
Keep callbacks fast: Anything that runs inside before_save or before_create holds the database transaction open. Long-running work or external API calls should move to after_commit or a background job.
Avoid N+1 queries: If a callback touches associations, preload them with includes or preload in the query that loaded the records.
Prefer validations for data rules: Use Active Record validations for required fields and format checks; reserve callbacks for side effects and transformations that validations cannot express.
Push complex logic out of models: Workflows involving multiple models, external services, or conditional steps belong in service objects, form objects, or command objects, not callbacks.
Watch after_initialize: It runs on every instantiation, including when ActiveRecord hydrates records from the database, so heavy logic there can silently slow down queries.
Use after_commit for external side effects: Sending email, writing to a third-party API, or expiring caches from after_create can happen even if the transaction later rolls back; after_commit prevents that.
18
How does the N+1 query problem occur in Ruby on Rails, and what techniques can you use to prevent it?
KnownRevise
The N+1 query problem occurs when Rails retrieves a collection of records and then runs an additional query for each record when an associated record is accessed. Active Record eager-loading methods such as includes, preload, and eager_load can prevent these repeated queries.
Read full answerHide full answer
Applies toRails 8
How the Problem Occurs
Assume a Post has many Comment records. If a controller loads posts with Post.all and a view accesses post.comments inside a loop, Active Record first executes one query for the posts. It then normally executes one additional query for each post to load its comments.
Loading 100 posts could therefore produce 101 queries. As the collection grows, query count and response time grow with it, potentially making the page slow and placing unnecessary load on the database.
Similar problems occur when accessing other lazy-loaded associations in loops, such as comment.user for every comment or post.author for every post.
Diagnosing the Problem
Development logs: Look for the same SQL pattern repeated with different bound values.
Query counts: Compare the number of queries with the number of records being processed.
Association access in loops: Review calls to associations such as post.comments, comment.user, or post.author inside iteration blocks.
Production monitoring: Query-monitoring tools can group similar statements and identify unexpectedly high execution counts.
Scoped queries: An association scope does not automatically eager-load its records; the association must still be preloaded or joined.
Preventing It with Active Record
includes: The general-purpose eager-loading option. Rails normally loads the parent records and associated records in separate queries, using a join when necessary.
preload: Always loads the specified associations in separate queries. This is useful when predictable, join-free loading is preferred.
eager_load: Loads the association with SQL joins, generally producing one larger query. This is useful when records must be filtered or queried across associated tables.
Nested eager loading: includes(comments: :user) loads comments and their users without a query per comment.
Strict loading: strict_loading! raises ActiveRecord::StrictLoadingViolationError when code attempts to lazy-load an association unexpectedly. The model-level strict_loading_by_default setting applies strict loading to matching records automatically.
Method
Typical query behavior
Best use
includes
Separate queries normally; joins when required
General association eager loading
preload
Separate queries
Predictable, join-free loading
eager_load
SQL joins
Filtering or querying across associations
Practical Example
For posts that render both comments and each comment’s user, Post.includes(comments: :user) normally loads the posts, comments, and users with a small, bounded number of queries instead of issuing one user query per comment.
If only comments are rendered, eager-load only comments with Post.includes(:comments). Each additional association increases the amount of data fetched, so loading unnecessary relationships can replace an N+1 problem with excessive database work or excessive memory use.
Best Practices and Common Pitfalls
Eager-load only what is needed: Include only associations that the response actually accesses.
Paginate large collections: Eager loading does not make rendering an unlimited number of records efficient.
Choose methods deliberately: Use preload for separate-query loading and eager_load when joined filtering or joined querying is required.
Watch aggregate methods: post.comments.count normally performs a database query even when the association is loaded. Use post.comments.size when the loaded collection’s size is sufficient.
Consider duplicate rows: Join-based eager loading can repeat parent columns, particularly with collection associations. eager_load can also produce larger result sets.
Keep conditions in the database: Do not fetch unrestricted collections and filter them in Ruby merely to avoid eager loading.
Verify query counts: Add automated query-count assertions where an N+1 regression would have a serious performance impact.
19
What is the recommended approach for handling database transactions within a Ruby on Rails application?
KnownRevise
The recommended Rails approach is to place related database writes in a transaction block on the relevant model or ActiveRecord::Base. Let exceptions roll the work back, keep the block short, and perform non-database side effects only after the commit.
Read full answerHide full answer
Applies toRails 8
How Rails Transactions Work
ActiveRecord::Base.transaction and model-level transactions such as User.transaction group database operations into one atomic unit:
The transaction begins when the block starts and commits if the block completes normally.
If an exception escapes the block, Rails rolls back the transaction and re-raises the exception.
A transaction operates on one database connection. Models using another database are not made part of the same atomic transaction merely because their calls appear in the same block.
Isolation depends on the database’s configuration; a transaction does not automatically prevent all concurrent modifications.
Nested Transactions and Failure Handling
Rails supports nested transaction calls, but their behavior depends on the options used:
Default nesting: The inner call joins the outer transaction and does not provide an independently committed inner transaction.
requires_new: true: When an outer transaction is already active, Rails creates a savepoint. At the outermost level, it starts a regular transaction.
Ordinary exception: The transaction is rolled back and the exception propagates to the caller.
ActiveRecord::Rollback: When raised inside the transaction block, Rails rolls back the current transaction or savepoint and swallows the exception, allowing execution after the block to continue.
Validation failure: save!, create!, and update! raise an exception that can trigger rollback. By contrast, save returns false without raising, so it does not automatically roll back other changes in the transaction.
Recommended Practices
Define the boundary around one business operation: Include all writes that must succeed or fail together, but avoid unrelated work.
Prefer bang methods: Use save! or update!, or explicitly handle a false result, so a failed write cannot accidentally allow the transaction to commit.
Keep transactions short: Long-running computation, file processing, and HTTP calls increase lock duration and contention.
Defer external side effects: Do not send emails, call external APIs, or enqueue untrusted work inside the transaction when those actions must happen only after a successful commit. An after_commit callback can be used for suitable model-bound side effects.
Use consistent lock ordering: When using pessimistic locking with lock, acquire locks in a consistent order across concurrent operations to reduce deadlocks. Locking must occur within a transaction.
Treat multiple databases explicitly: Transactions do not provide automatic distributed atomicity across separate databases.
Common Pitfalls
Catching an exception and ignoring it can allow the transaction to commit partial work; handle the failure deliberately.
Assuming save returning false rolls back the transaction is incorrect.
Performing slow or external work inside the transaction can degrade concurrency and produce side effects that cannot be undone by a rollback.
Locking rows in different orders across competing operations can cause deadlocks even though the individual queries are valid.
20
How do polymorphic associations operate in Ruby on Rails, and which design scenarios benefit most from this pattern?
KnownRevise
A polymorphic association lets one model belong to several possible parent models through one association. In Rails, the child record stores the target’s ID and type in separate columns, and Active Record uses those values to resolve the associated object.
Read full answerHide full answer
Applies toRuby on Rails 8
Core Concept and Use Cases
This pattern avoids separate nullable foreign keys or target-specific join tables when the same relationship must point to unrelated domain models.
Comments: One comments table can associate each comment with a post, video, product, or other permitted model.
Attachments: A shared attachment model can associate files with users, orders, documents, and similar records.
Notifications: A notification can reference different triggering resources without creating a separate table for every source type.
Activity or audit records: Events may target different kinds of resources while sharing common metadata and behavior.
Polymorphism is most useful when the relationship and its behavior are genuinely shared, rather than merely convenient.
Stored type: commentable_type normally contains the associated model’s base class name, such as Post. With single-table inheritance, the base class name is used by default.
Indexing: index: true creates a composite index over the type and ID columns. Keeping this index is recommended because association lookups commonly filter by both values.
Column types: Rails 8 normally creates the reference ID as a bigint and the type as a string. The physical string limit depends on the database adapter and schema configuration.
Foreign keys: A conventional foreign-key constraint cannot point one column at whichever of several parent tables contains the target row.
Model Implementation
Child side: belongs_to :commentable, polymorphic: true enables comment.commentable to resolve the associated record.
Parent side: has_many :comments, as: :commentable exposes post.comments and video.comments. These declarations are optional inverse navigations rather than requirements for resolving the child association.
Type restriction: When only known models are valid, validate commentable_type against an explicit allowlist so untrusted type names are not accepted.
Presence and cleanup: The required belongs_to association validates that a target is present. Configure dependent on the inverse association when deleting a parent should also delete or otherwise handle its children.
Code Example
# db/migrate/20231010000000_create_comments.rb
class CreateComments < ActiveRecord::Migration[8.0]
def change
create_table :comments do |t|
t.text :body
t.references :commentable, polymorphic: true, index: true
t.timestamps
end
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
validates :commentable_type, inclusion: { in: %w[Post Video] }
end
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments, as: :commentable, dependent: :destroy
end
# app/models/video.rb
class Video < ApplicationRecord
has_many :comments, as: :commentable, dependent: :destroy
end
Performance and Pitfalls
N+1 queries: Lazy-loading each comment’s commentable record can produce one query per row. Use includes(:commentable) when preloadable associations are appropriate.
Multiple preload queries: Preloading a polymorphic association issues a separate query for each distinct target type, so very fragmented data can still produce several queries.
Referential integrity: Because one polymorphic pair cannot use a normal multi-table foreign key, use validations, appropriate inverse dependent behavior, and application or database-specific cleanup mechanisms.
Unrestricted types: Do not accept arbitrary association type names from users. Use a fixed allowlist whenever the permitted parent models are known.
21
How do helper methods support the presentation layer in Rails views?
KnownRevise
View helpers are methods available to templates that encapsulate reusable presentation logic and generate view-related output. They keep views readable by moving shared formatting, markup, asset, and navigation logic out of templates while leaving application workflows in controllers and domain objects.
Read full answerHide full answer
Applies toRails 8
How Helpers Work
Rails exposes helper methods to templates through the view context. A view can call an available helper in the same way it calls a local or instance variable:
link_to creates an anchor element.
form_with builds a form and includes Rails’ CSRF protection.
content_tag creates an HTML element with attributes and content.
image_tag and asset_path resolve images and other assets.
number_to_currency converts a numeric value for localized currency display.
In a standard Rails 8 application, helper modules in app/helpers are included in views by default through config.action_controller.include_all_helpers, which defaults to true. Rails also provides built-in helper modules for links, forms, assets, dates, numbers, text, and sanitization.
Controllers can expose selected controller methods to templates with helper_method. This is useful for a small display value, but it is not usually the best place for substantial business rules.
Why Helpers Matter
Helpers provide several practical benefits:
Reusable presentation logic: Formatting or markup shared by many templates can be implemented once.
Cleaner views: Templates focus on arranging content rather than repeating complicated string or HTML construction.
Framework integration: Built-in helpers handle concerns such as escaping, asset resolution, URL generation, forms, and CSRF tokens.
Consistent output: Application-specific formatting can give related screens a uniform appearance.
Easier maintenance: A shared formatting change can be made in one place rather than across numerous templates.
For example, a custom helper could produce a consistent status label throughout an administration interface. It should receive the information it needs and return the rendered representation rather than querying the database or changing application state.
Safe HTML Handling
Rails escapes interpolated values by default, reducing the risk of cross-site scripting. Helpers should continue to return ordinary strings unless they deliberately produce trusted HTML.
sanitize removes unsafe markup while allowing configured safe elements and attributes.
html_safe marks a string as safe and bypasses automatic escaping; use it only with trusted or sanitized content.
raw outputs content without escaping and must not receive unsanitized user input.
Avoid constructing HTML through unescaped string concatenation. Prefer helpers such as content_tag, tag, link_to, and form_with, which are easier to review and safer to maintain.
Best Practices and Limitations
Keep helpers focused on presentation, not business workflows, authorization, database access, or persistence.
Prefer explicit method names and parameters over helpers that depend on unexpected controller state.
Move reusable formatting into helpers or, when appropriate, a dedicated presenter or view component.
Do not treat a helper as a substitute for authorization. A hidden link is not protection; controllers must enforce access rules.
Remember that changing a shared helper can affect every template that uses it, so include meaningful tests for shared helpers.
22
What distinguishes ERB, Haml, and Slim from one another when used as Rails view templates?
KnownRevise
Template syntax and developer ergonomics are the main differences among ERB, Haml, and Slim. ERB embeds Ruby inside HTML, while Haml and Slim express markup through indentation-based, less verbose syntax; all three can integrate with the same Rails helpers, layouts, partials, and view rendering pipeline.
Read full answerHide full answer
Applies toRails 8 with Haml 6.x and Slim 4.x
Core Differences
Template
Markup style
Ruby integration
Main characteristic
ERB
Standard HTML with embedded Ruby
<% ... %> and <%= ... %>
Most conventional and widely understood
Haml
Indentation-based
Ruby statements and output use Haml-specific prefixes
Concise, but whitespace and indentation are significant
Slim
Indentation-based and more CSS-like
Ruby statements and output use Slim-specific prefixes
Compact and readable, especially for small templates
ERB: Embedded Ruby
ERB, which stands for Embedded Ruby, keeps the HTML structure visible:
<% ... %> executes Ruby logic.
<%= ... %> renders a value.
ERB is the default Rails template format and requires no additional template-handler gem.
Common filenames include index.html.erb.
Because HTML remains explicit, ERB works particularly well in templates containing substantial markup, inline JavaScript, or complex conditional structures. Its familiarity also makes it easier for developers moving into Rails from other server-rendered frameworks.
Haml and Slim: Indentation-Based Templates
Both Haml and Slim remove explicit closing tags and rely on indentation to establish nesting.
Haml commonly represents a paragraph with %p, followed by its content. It is concise, but incorrect indentation can change the structure of the generated HTML.
Slim uses a more CSS-like syntax, representing a paragraph with p. It generally produces less markup-like source than Haml while retaining indentation-based organization.
Both formats use a dash-prefixed line for Ruby logic and an equals-prefixed line for output.
Rails helpers continue to work normally in these templates. For example, a partial named index.html.haml or index.html.slim can still be rendered through the same Rails partial API and can use form builders, translation helpers, and route helpers.
Escaping and Security
All three engines support HTML escaping for rendered values by default under Rails. Haml and Slim provide engine-specific syntax for explicitly unescaped output, as does ERB.
Treat normal output as untrusted and allow Rails to escape it.
Use raw output only for trusted, sanitized HTML.
Never mark user-provided values as safe merely to preserve formatting.
Continue to use strong parameters, model validations, and normal Rails escaping regardless of the template language.
Template choice does not replace input validation or output sanitization.
Rails Integration and Performance
ERB is built into Rails. Haml and Slim are available through the haml-rails and slim-rails gems, respectively; those integrations register the template handlers used for .haml and .slim files.
Once a template is loaded, all three engines ultimately generate Ruby that Action View executes. Consequently, their runtime performance is often broadly comparable; differences in an application more commonly come from the rendered content, helper calls, database access, or excessive partial rendering.
Use includes, preload, or eager_load to address N+1 queries regardless of the template format. Avoid rendering large collections directly when pagination or a paginated API response would be more appropriate.
Choosing a Format
Choose ERB for default Rails development, maximum familiarity, and templates with complex HTML.
Choose Haml when concise, indentation-based markup is preferred by the team and consistent formatting can be enforced.
Choose Slim when a compact, CSS-like syntax is desired and the team is comfortable with its conventions.
For an existing application, mixing formats is technically possible, but using one main format reduces tooling and maintenance overhead. Whichever format is selected, keep business logic in models or service objects and reserve views for presentation.
23
How do you render reusable view templates using partials in a Ruby on Rails application?
KnownRevise
In Ruby on Rails, partials are reusable view fragments stored in app/views that allow you to modularize HTML templates and avoid duplication across pages. They are rendered using the render method and accept local variables to maintain context independence.
Read full answerHide full answer
Applies toRails 8
Understanding Partials
Partials reside in the app/views directory and are distinguished by a filename beginning with an underscore, such as _user_card.html.erb. This naming convention keeps them out of normal action-template lookup. They can be invoked from other views, layouts, or controller responses to compose pages from smaller components.
Rendering Syntax and Locals
The simplest way to render a partial is to call render with the partial name, excluding the leading underscore. You can pass data to the partial using local variables, which ensures the fragment remains independent of the surrounding view’s instance variables. Explicitly passing locals makes the partial’s dependencies clear and improves its reusability.
Rendering Collections
Rails can render a collection directly. For example, render @users can infer the users/_user partial and expose each record as user. When using a custom partial, specify it explicitly with partial:. Because the local variable normally defaults to the partial’s name, use as: when the partial expects a different variable name.
# app/controllers/users_controller.rb
def index
@users = User.order(:id).limit(10)
@user = @users.first
end
Best Practices and Common Pitfalls
Avoid Heavy Logic in Partials: Keep business logic in models or controllers. Partials should primarily handle presentation, although simple conditions and loops may be appropriate.
Prevent N+1 Queries: If a partial accesses an association such as user.profile, preload that association with User.preload(:profile). Use includes or eager_load when appropriate, and avoid loading associations the partial does not use.
Explicit Locals: Prefer passing explicit locals over relying on instance variables such as @user inside partials to improve testability and reusability.
Turbo Integration: In Rails 8, partials can also be rendered inside Turbo Stream responses to update specific DOM fragments without reloading the entire page.
Naming Conventions: Ensure the partial path matches the render call. Within app/views/users/index.html.erb, render "user_card" resolves to app/views/users/_user_card.html.erb.
24
How does Turbolinks change page navigation and the overall behavior of a Rails application?
KnownRevise
Turbolinks is a legacy client-side library that makes navigation between Rails pages feel faster by replacing the browser’s full-page reload with a background request for the next document. It is deprecated and no longer maintained; Turbo, specifically Turbo Drive, is its modern replacement in Rails 8.
Read full answerHide full answer
Applies toTurbolinks 5 (legacy); Rails 8 with Turbo 8
How Turbolinks Works
When a user activates an eligible internal link or form submission, Turbolinks:
Intercepts the request in the browser.
Fetches the destination HTML using a background HTTP request.
Replaces the current document.body with the new page’s body.
Reconciles the document’s <head>, including stylesheets and scripts.
Updates browser history using the History API.
Restores scroll position when navigating backward or forward.
The server side remains conventional: Rails routes, controllers, strong parameters, validations, callbacks, and database operations continue to work normally. Turbolinks does not convert the application into an API-only backend; it retrieves and swaps complete HTML documents.
Effects on a Rails Application
Reduced full reloads: The browser does not replace the page’s JavaScript environment, so global JavaScript state can remain in memory.
Faster perceived navigation: The next page is retrieved without the visual interruption of a normal browser reload.
Persistent JavaScript state: Variables stored on window survive navigation, which can either be useful or cause unexpected behavior.
Repeated page initialization: The new body and its scripts may be initialized on every visit. Consequently, event handlers can be registered repeatedly, and references to old DOM elements become invalid.
Normal server rendering: Each destination still returns an HTML document, preserving the standard Rails MVC architecture.
History and scroll restoration: Back and forward navigation is enhanced, and previously visited scroll positions can be restored. An ordinary browser reload remains a normal reload and creates a fresh JavaScript environment.
Turbolinks dispatches events such as turbolinks:before-visit, turbolinks:render, and turbolinks:load. Legacy applications often use turbolinks:load to initialize page-specific behavior instead of relying only on DOMContentLoaded.
Turbolinks and Modern Rails Navigation
Mechanism
Navigation behavior
Current status
Turbolinks 5
Fetches complete HTML responses, replaces the body, and merges the head without replacing the window
Deprecated and no longer maintained
Turbo Drive
Provides modern full-page navigation using Turbo
Current default approach in Rails 8
Turbo Frames
Updates a targeted portion of a page independently
Useful for focused interactions
Turbo Streams
Applies DOM changes from responses or broadcasts
Useful for dynamic and real-time updates
Rails 7 made Hotwire, including Turbo, the default replacement for the older Turbolinks setup. Rails 8 continues that approach through the turbo-rails integration.
Migration and Common Pitfalls
For a Rails 8 application, use turbo-rails and Turbo rather than adding the obsolete turbolinks gem.
Replace legacy attributes such as data-turbolinks="false" with data-turbo="false".
Replace data-turbolinks-track="reload" with data-turbo-track="reload" when a full reload is required.
Replace turbolinks:load listeners with turbo:load listeners when migrating.
Use Turbo Frames when only a small region needs to change; full-document navigation is unnecessary for every interaction.
Avoid assuming that page JavaScript starts with a completely fresh global state.
Test form validation errors, redirects, file downloads, back-button behavior, and third-party scripts explicitly.
Keep authentication, authorization, and input validation enforced on the server; client-side navigation provides no security boundary.
25
How does Webpacker integrate with a Rails application to compile, serve, and reference JavaScript assets?
KnownRevise
Webpacker is a legacy Rails integration that connects webpack to Rails’ asset workflow: webpack compiles JavaScript entry points, a manifest maps those entries to emitted files, and javascript_pack_tag references the resulting packs from views. Rails 8 applications should use Propshaft with importmap-rails, or jsbundling-rails when a client-side bundler is required.
Read full answerHide full answer
Applies toRails 8; Rails 6 with Webpacker 5.4.4 for the legacy workflow
Current Status
Webpacker is deprecated and is not part of Rails 8’s default asset stack.
Rails 7 introduced importmap-rails and jsbundling-rails as the official alternatives for projects leaving Webpacker.
Rails 8 applications normally use Propshaft as their asset pipeline and importmap-rails for browser-native JavaScript modules. A project that needs webpack, esbuild, Rollup, Bun, TypeScript, or JSX can use jsbundling-rails instead.
Hotwire is independent of the asset pipeline. Turbo and Stimulus can be loaded through an importmap or bundled with jsbundling-rails.
How Legacy Webpacker Worked
Webpacker created a bridge between Rails and webpack:
Configuration: config/webpacker.yml defined environment-specific source paths, output paths, webpack settings, and development-server behavior.
Entry points: Files under app/javascript/packs, such as application.js, served as webpack entry points.
Compilation: Webpack processed JavaScript and configured loaders for TypeScript, CSS, images, and other imported assets.
Dependencies and splitting: Pack entry points could import npm packages and use webpack features such as code splitting, dynamic imports, and source maps.
Manifest: Webpacker produced public/packs/manifest.json, mapping pack names to emitted files and their content digests.
View integration: javascript_pack_tag "application" consulted that manifest and generated the appropriate script element for the requested pack.
Compile, Serve, and Reference Flow
Compile: The generated bin/webpack command performed a one-off webpack build. For a production deployment, RAILS_ENV=production bin/webpack generated deployable files under public/packs.
Serve during development: bin/webpack-dev-server ran separately from the Rails server, watched sources, recompiled incrementally, and served development assets with hot reloading support.
Serve in production: The compiled files and manifest were deployed with the application. Rails’ static-file support or, more commonly, the front-end web server delivered the files from public/packs.
Reference from a view: javascript_pack_tag used the manifest to find the emitted JavaScript and emitted a script tag. In development-server mode, it referenced the development server’s asset URL.
Webpacker therefore did not replace Rails’ view helpers or asset manifest mechanism; it generated the files that those mechanisms referenced.
Rails 8 Alternatives
Propshaft with importmap-rails: Propshaft prepares, fingerprints, and exposes application assets, while the importmap tells the browser how to resolve module specifiers. The layout can render the importmap and entry module with javascript_importmap_tags.
Importmap packages: An npm package can be used without a client-side bundler only when a compatible browser-loadable ES module is available. It may need to be vendored or downloaded with an importmap-rails task before it is pinned.
jsbundling-rails: This installs and runs the selected JavaScript bundler, normally writing output to app/assets/builds. Propshaft then incorporates that output into Rails’ asset pipeline.
CSS: Rails 8 can serve CSS through Propshaft, while tailwindcss-rails or cssbundling-rails provides a Tailwind or other CSS build workflow.
Best Practices and Migration Guidance
Do not combine javascript_pack_tag and javascript_importmap_tags unless the application deliberately supports both delivery systems during a migration.
Keep package manifests, lockfiles, importmap pins, and bundler configuration under version control.
Build production assets during deployment and verify that the deployment’s static-file server delivers the generated files.
Never use Webpacker’s development server as a production service; compile production assets ahead of time.
Migrate legacy packs to importmap pins or a jsbundling-rails entry point, then replace javascript_pack_tag with the appropriate current Rails helper.
26
How do `data-*` attributes function in Rails views, and what is the recommended way to add and consume them?
KnownRevise
data-* attributes are standard HTML attributes that expose information or behavior hooks to JavaScript without coupling a Rails view to a particular DOM structure. In Rails, you can write them directly in ERB or generate them with the data: option supported by tag helpers, then consume them with the DOM API or Stimulus.
Read full answerHide full answer
Applies toRails 8 with Hotwire (Turbo 8.x and Stimulus 3.x)
How They Work
HTML attributes beginning with data- belong to the global data-* namespace. They commonly identify elements, carry configuration, or provide hooks for JavaScript behavior.
Common examples include:
data-user-id="42" identifies the related record.
data-controller="user-details" connects a Stimulus controller.
data-action="click->user-details#show" declares a Stimulus action.
data-turbo="false" opts an applicable link or form out of Turbo navigation.
Browsers expose these attributes through the element’s dataset object. The hyphenated name becomes a camel-cased property:
Rails helpers convert underscored keys in the data: hash into hyphenated attribute names. For example, user_id becomes data-user-id.
The tag helper and normal <%= ... %> ERB output escape attribute values by default. This helps prevent user-controlled content from breaking out of an attribute. Avoid raw or html_safe around untrusted values.
Using Them with Stimulus
Stimulus provides a higher-level API for controller values. A controller named user-details with a userName value can read an attribute such as data-user-details-user-name-value.
For ordinary data that is not a controller value, Stimulus elements can access this.element.dataset directly.
Turbo-specific attributes also follow this pattern. Common examples include data-turbo, data-turbo-method, and data-turbo-confirm. The older Rails UJS data-confirm convention is legacy in Rails 8; use Turbo’s data-turbo-confirm when confirmation is required.
Code Example
The following ERB creates a button that Stimulus can use:
It renders a button containing data-controller="user-details", data-action="click->user-details#show", data-user-id, and data-user-details-user-name-value, with the values populated from @user.
The matching controller file, app/javascript/controllers/user_details_controller.js, declares static values = { userName: String }. Its show() method reads the ordinary identifier through this.element.dataset.userId and the typed controller value through this.userNameValue.
Best Practices and Common Pitfalls
Keep values server-authoritative: Never trust a data-* value for authorization, price calculation, ownership checks, or other security decisions.
Do not treat data attributes as secrets: Their contents are visible in the rendered HTML.
Avoid large embedded payloads: Store an identifier in data-* and retrieve the required information from the server when necessary.
Prefer Stimulus values for typed configuration: They provide clearer APIs and support values such as String, Number, and Boolean.
Do not build SQL from attribute values: Pass validated identifiers to Active Record or use bound query parameters.
Use button_to for destructive actions: It generates a non-GET form and is generally more robust than JavaScript-dependent link behavior.
Keep selectors resilient: Attribute selectors such as [data-controller="user-details"] are usually more stable than depending on generated CSS classes or deeply nested markup.
27
How do unit, functional, and integration tests differ when testing a Rails application?
KnownRevise
The main distinction is the scope and isolation of each test: unit tests verify one small component, functional tests exercise a specific application feature or request path, and integration tests verify that multiple components work together correctly. In Rails 8, these categories often overlap in terminology, so the test’s dependencies and execution boundary are more important than its directory name.
Read full answerHide full answer
Applies toRails 8
Core Differences
Test type
Primary focus
Typical dependencies
Main question answered
Unit test
One class, method, or business rule
Usually none or minimal collaborators
Does this component behave correctly in isolation?
Functional test
One feature, controller action, or request path
The component plus its immediate framework dependencies
Does this feature process a request and produce the expected response?
Integration test
Several components working together
Models, database, routing, controllers, views, or services
Do the application’s parts interact correctly?
Unit Tests
A unit test targets the smallest useful unit of behavior, such as:
A model method that calculates a total
A service object that changes application state
A validator or business-rule method
A query object that builds criteria for a relation
The unit should remain understandable without making an HTTP request. Tests may instantiate the object directly and assert on its return value, state changes, or raised exceptions.
Isolation: External collaborators such as HTTP APIs are commonly replaced with test doubles. Pure units normally avoid the database, while database-backed Rails tests use the isolated test database and transactional tests. Background jobs can use Active Job’s test adapter when job behavior is outside the unit’s scope.
Speed: Unit tests are usually the fastest because they perform less setup and fewer framework operations.
Granularity: A failure usually points directly to the component responsible for it.
Limitation: Passing unit tests do not prove that Rails correctly routes requests, loads records, or connects components.
Rails uses Minitest by default. A pure unit test may inherit from Minitest::Test; tests requiring Rails-specific support typically inherit from ActiveSupport::TestCase. RSpec is also widely used in Rails projects.
Functional and Request Tests
A functional test checks whether a particular feature behaves correctly through its framework-facing interface. Historically, this meant controller tests that sent a request to an action and examined the response, redirects, rendered body, or session.
Rails 8 does not define a separate functional-test base class or category. The current equivalent is usually a focused request test, commonly stored under test/requests and implemented with ActionDispatch::IntegrationTest. Request tests are integration tests in Rails’ taxonomy, but interview usage often calls a focused request test “functional.”
These tests are appropriate for questions such as:
Can an authenticated user update their profile?
Does submitting invalid form data render validation errors?
Is an unauthorized request rejected or redirected?
Does the response contain the expected status, body, or location?
They are generally narrower in intent than a complete user workflow.
Integration and System Tests
An integration test verifies that multiple application components cooperate. Depending on the project, it may include the database, models, routes, controllers, views, services, jobs, or external adapters.
Rails provides two relevant levels:
Request-level integration: Exercises the application’s HTTP stack through ActionDispatch::IntegrationTest, including routes, controllers, views, and the test database.
System-level testing: Uses ApplicationSystemTestCase and a real browser through Capybara and Selenium. It validates user-visible workflows such as registration, checkout, or navigation between pages.
System tests provide stronger end-to-end confidence but are slower and more susceptible to timing, browser, and environment problems. They should not replace fast unit or request tests.
Rails 8 Test Organization
Rails 8 conventionally organizes tests into directories such as test/models, test/controllers, test/requests, test/integration, test/system, test/jobs, and test/mailers.
These paths are organizational conventions; Rails does not infer a test’s base class from its location. Generated tests explicitly inherit from classes such as ActiveSupport::TestCase, ActionDispatch::IntegrationTest, or ActionDispatch::SystemTestCase.
The directory name therefore does not define the test’s conceptual scope. For example:
A test in test/models can be a fast unit test or a database-backed integration test.
A test in test/integration can combine several request flows.
A test in test/system coordinates the browser, server, database, and application components.
Best Practices and Common Pitfalls
Test behavior, not implementation details: Avoid assertions that depend unnecessarily on private methods or exact SQL formatting.
Keep unit tests isolated: Prefer explicit collaborators or test doubles for external dependencies and avoid unnecessary database work.
Use integration tests for framework boundaries: Verify routing, parameter handling, authorization, redirects, and persistence through realistic requests.
Prevent N+1 queries: Use includes or preload when the feature needs related records, and consider query-count assertions where appropriate.
Do not over-mock integration paths: Excessive mocking can make an integration test pass without proving that the real components interact correctly.
Balance the test pyramid: Maintain many focused unit tests, a practical set of request and integration tests, and a smaller number of critical browser-driven system tests.
28
Describe the role RSpec plays in testing a Ruby on Rails application and how a team would use it in practice.
KnownRevise
RSpec is a testing framework that gives Ruby on Rails teams a readable, behavior-focused way to describe and verify application functionality. It supports unit, integration, and browser-level testing while integrating with Rails models, requests, jobs, mailers, and other components.
Read full answerHide full answer
Applies toRuby on Rails 8 with RSpec 3.x and rspec-rails 8.x
What RSpec Provides
RSpec organizes tests into groups such as describe and context, with individual examples defined by it. Expectations use expect(...) together with matchers such as eq, include, change, be_valid, and have_http_status.
Rails itself uses Minitest by default, so RSpec is an optional choice. Teams commonly select it because its syntax and supporting libraries make complex test scenarios easier to read and organize.
Rails Setup and Integration
RSpec is not bundled with Rails. A team adds rspec-rails to the Gemfile, normally in the development and test groups, and runs the rspec:install generator through bin/rails generate rspec:install.
The generator creates spec/spec_helper.rb, spec/rails_helper.rb, and .rspec. The .rspec file normally contains --require spec_helper. The RSpec-specific configuration belongs in spec_helper.rb, while rails_helper.rb loads the Rails application and activates the Rails integrations. Specs that exercise application components generally require rails_helper.
Rails’ Minitest-based test/ directory can coexist with the spec/ directory, although a team that standardizes on RSpec may remove it.
Common Spec Types
Model specs: Test Active Record validations, associations, callbacks, scopes, and public model methods without involving HTTP or rendering.
Request specs: Exercise routing, controllers, middleware, and response rendering. They are usually the best level for authentication, authorization, and JSON API behavior that does not require browser JavaScript.
System specs: Use Capybara and a browser driver such as Selenium to test complete user journeys, including JavaScript-driven interactions.
Other component specs: rspec-rails supports focused specs for jobs, mailers, helpers, views, and routing.
The older feature-spec style is legacy in Rails 8; teams should generally prefer system specs for browser-level behavior.
How a Team Uses It
Teams commonly follow an arrange-act-assert structure: create the relevant records, perform one action, and assert the observable result. Factories such as FactoryBot can provide test data, while RSpec examples document each supported behavior and its important edge cases.
Rails-backed specs normally use transactional fixtures by default, wrapping each example in a database transaction that is rolled back afterward. System specs may need a separate cleanup strategy because browser execution does not share the example’s transaction.
The suite is usually run with bundle exec rspec. A team can run the complete suite during continuous integration, target an individual file, or target one example by appending its line number. Slow system specs may be separated into a second CI job, but they should still run regularly because they protect critical user journeys.
Best Practices and Common Pitfalls
Prefer observable behavior: Assert responses, rendered content, persisted state, and public methods rather than internal implementation details.
Choose the narrowest useful integration level: Model specs are fast, request specs cover HTTP behavior, and system specs are appropriate when browser interaction matters.
Test validations, callbacks, and strong parameters in their proper contexts: Do not duplicate their implementation details inside test helpers.
Guard against N+1 queries: Exercise collection and association paths, and use Rails’ includes, preload, or eager_load when appropriate.
Keep examples focused and deterministic: Avoid multiple unrelated assertions, uncontrolled time, and unnecessary mocking.
Do not mock owned internals indiscriminately: Excessive stubbing can hide integration defects that users would encounter.
Keep the suite fast: Reserve browser tests for behavior that genuinely requires them, and give failures clear messages that identify the violated expectation.
29
What exactly does a test runner do, and which test runners do Rails developers typically use?
KnownRevise
A test runner is the executable that discovers tests, invokes their setup/test/teardown lifecycle, captures failures and errors, and reports results with an exit status. In Rails, Minitest supplies the assertions and test DSL, while the Rails test task coordinates execution; RSpec can provide an alternative runner and DSL.
Read full answerHide full answer
Applies toRails 8 with Minitest 5.x; system tests use Capybara 3.x with Selenium WebDriver 4.x
What a Test Runner Actually Does
Discovery: Finds tests according to the framework’s conventions. Rails’ Minitest integration uses files such as test/models/user_test.rb; RSpec uses its own spec/*/_spec.rb convention.
Lifecycle: Creates test instances, invokes setup and teardown hooks, runs each test method, and records its result.
State isolation: Rails’ default transactional tests wrap supported Active Record tests in a transaction and roll it back afterward. This is a Rails integration feature rather than a universal runner guarantee, and suites can opt out.
Assertions and expectations: Minitest provides assertions such as assert_equal, while RSpec provides expectations such as expect(...).to eq(...). Rails adds helpers such as assert_difference, assert_enqueued_with, and assert_emails.
Reporting and exit status: Reports successful tests, failures, errors, backtraces, and file-and-line locations. A failing suite returns a non-zero exit status so CI can detect it.
Selection and parallelism: The Rails test command accepts a file, line number, or name pattern. Parallelization is configured separately through Rails’ parallelize helper and may use processes or threads.
Minitest: The Rails Default
Bundled framework: ActiveSupport::TestCase inherits from Minitest::Test, so generated Rails applications can run tests with bin/rails test without adding a separate test-runner gem.
Rails helpers: Supports fixtures, transactional tests, assert_difference, Active Job assertions, and Action Mailer assertions.
Conventional layout: Generated applications use directories including test/models, test/controllers, test/integration, and test/system, with test/test_helper.rb loading the test environment.
Parallel testing: Rails 8’s generated test helper includes parallelize(workers: :number_of_processors). Process workers receive separate test databases, while thread-based parallelization is also supported.
System tests: Generated applications include Capybara and Selenium WebDriver in the test group and commonly configure headless Chrome. System tests are excluded from bin/rails test; use bin/rails test:system, or bin/rails test:all—introduced in Rails 7.1—to include them.
Generators: Rails 8’s authentication generator, introduced in Rails 8, emits Minitest tests alongside the generated authentication code.
RSpec and Other Options
RSpec: The rspec-rails gem provides an alternative runner, describe/it syntax, expectations, Rails-specific matchers, and generators. Run it with bundle exec rspec. It can coexist with Minitest files, although most teams standardize on one style.
System versus request specs: System specs use Capybara for browser interaction. Request specs use Rails integration-test APIs such as get and post and do not require Capybara.
Cucumber: A BDD runner that executes Gherkin scenarios and invokes step definitions. Teams may implement browser-related steps with Capybara, but Capybara is not part of Cucumber itself.
Additional tooling: The parallel_tests gem provides process-based parallelization, while Shoulda Matchers adds concise matchers to Minitest or RSpec and is not a runner.
Aspect
Minitest
RSpec
Availability
Included with Rails
Added through rspec-rails
Command
bin/rails test
bundle exec rspec
Style
Assertions and test blocks
Expectations with describe and it
Rails integration
Native test tasks, fixtures, transactional tests, and parallelization
Rails matchers, fixtures, request/system helpers, and generators
Typical choice
Default for new Rails applications
Common for teams that prefer specs and richer matchers
Best Practices and Pitfalls
Run the intended scope: bin/rails test does not include system tests, so CI must invoke the system-test task or test:all when browser coverage is required.
Express side effects clearly: Use assert_difference around the action to verify the expected database or job-count change.
Avoid fixed sleeps in system tests: Capybara’s waiting matchers, including assert_selector and assert_text, retry until the expected UI state appears or the timeout expires.
Keep fixtures valid: Fixtures bypass model validations, so invalid fixture data can conceal broken application behavior.
Test public behavior: Prefer exercising private implementation details indirectly through public interfaces.
Balance CI feedback: Use --fail-fast during local iteration when appropriate, but allow CI to run the full suite so one failure does not hide others.
30
In a Rails 8 application, which commands should I use to run one test file or target an individual test case within it?
KnownRevise
Use the Rails test runner through bin/rails test. Pass a test file path to run the entire file, append :line_number to target a specific test, or use the -n option to select a test method by name.
Read full answerHide full answer
Applies toRails 8 with Minitest
Selecting Tests
Goal
Command
Run every test in a file
bin/rails test test/models/user_test.rb
Run the test defined at a particular line
bin/rails test test/models/user_test.rb:42
Run a test by its Minitest method name
bin/rails test test/models/user_test.rb -n '/^test_user_is_valid$/'
Show detailed test names and results
bin/rails test test/models/user_test.rb -v
Run these commands from the root of the Rails application. Rails uses the test environment for the test runner and loads the application’s test configuration before executing the selected tests.
How Test Selection Works
File selection: Supplying a path such as test/models/user_test.rb restricts execution to that file. Rails 8 uses Minitest as its default test framework.
Line selection: The :42 suffix identifies a test whose declaration begins on line 42 of the file. It is generally more durable to target the test declaration rather than a line inside the test body.
Name selection: The -n option applies a regular expression to the generated Minitest method name. For example, Rails turns test "user is valid" into a method named test_user_is_valid.
Verbosity: The -v option is useful when you do not know the generated method name or want to confirm which test was selected.
Test Environment and Results
The bin/rails test command runs against the test environment, not the development or production environment. The test database must already be prepared and migrated before the suite runs.
When a selected test fails, Rails prints the failure details and the command exits unsuccessfully, making it suitable for CI pipelines. Test output can include the failing assertion, expected and actual values, relevant source locations, and the complete test-method name.
Code Example
The following example assumes a Rails test file containing a test declaration on line 5:
# test/models/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "user is valid" do
user = User.new(name: "Ada")
assert user.valid?
end
end
Run that file or the individual test with:
# Run every test in the file
bin/rails test test/models/user_test.rb
# Run the test declared on line 5
bin/rails test test/models/user_test.rb:5
# Run it by its generated Minitest method name
bin/rails test test/models/user_test.rb -n '/^test_user_is_valid$/'
Best Practices and Common Pitfalls
Prefer bin/rails test because it loads the application’s correct Bundler environment.
Use an anchored regular expression with -n when you intend to run exactly one method; an unanchored pattern may match multiple tests.
Recalculate line-based selectors after editing a test file because line numbers can shift.
If the project intentionally uses RSpec rather than Rails’ default Minitest runner, use the project’s RSpec commands instead; do not mix the two runners.
31
Explain how mocking and stubbing support automated tests, and how their responsibilities differ.
KnownRevise
Mocks and stubs are test doubles that replace real dependencies so tests can run quickly, deterministically, and in isolation. A stub supplies predetermined responses, while a mock also verifies that expected interactions occurred.
Read full answerHide full answer
Applies toRails 8; these concepts apply to Minitest, RSpec, and other Rails-compatible testing frameworks
How They Work
Tests often contain three parts:
Subject under test: The object or service being exercised.
Real dependency: An external service such as a payment provider, email server, HTTP API, or database.
Test double: A controlled replacement for that dependency.
A test double prevents the test from depending on network availability, third-party behavior, timing, or unrelated application state. This makes failures easier to diagnose because the test focuses on one unit of behavior.
Stubs Versus Mocks
Test double
Primary purpose
Verifies interactions?
Typical use
Stub
Return controlled values
No
Ignoring part of a dependency’s behavior
Mock
Stand in for a dependency and check how it is used
Yes
Confirming a service call, argument, or sequence
Fake
Provide a lightweight working implementation
Usually no
In-memory persistence or simplified infrastructure
For example, when testing an order service, a payment gateway can be stubbed to return success without contacting the provider. A mock can additionally assert that the service received the expected order ID and amount.
These categories are related but not universally identical across frameworks. Some frameworks combine them under a broader “mock” or “test double” API.
Why They Are Useful
Isolation: Tests do not require live payment, email, storage, or third-party services.
Speed: Local replacements execute much faster than external network calls.
Determinism: External failures and changing data cannot make an otherwise unrelated test flaky.
Edge-case control: A test can simulate declined payments, timeouts, malformed responses, or missing records.
Behavior verification: Mocks can detect regressions where a required event was never published or a persistence operation was omitted.
Focused unit tests: Each class can be tested without starting an entire Rails application or application server.
Best Practices and Common Pitfalls
Prefer real objects for core domain logic: Value objects, calculations, and standard Ruby or Rails behavior usually need no test doubles.
Mock at architectural boundaries: HTTP clients, gateways, clocks, queues, and external APIs are common candidates. Over-mocking can make tests tightly coupled to implementation details.
Keep scenarios realistic: Configure the minimum behavior needed by the unit under test, but ensure it resembles the real dependency’s contract.
Verify meaningful behavior: Avoid excessive assertions about method counts unless repetition, ordering, or exactly-once behavior is part of the requirement.
Do not replace the entire application: A test that stubs everything may pass while the integrated system is broken. Retain controller, model, job, and request tests that exercise Rails components together.
Use integration tests for wiring: Rails request tests are valuable for verifying routes, strong parameters, validations, callbacks, database behavior, and framework configuration.
Treat external API contracts separately: Stubbed HTTP responses do not prove that the third-party API still behaves as expected. Maintain contract or monitored integration tests against a controlled test environment where practical.
Avoid hiding business rules in mocks: If production code asks a mock for complex decisions, the test may no longer describe the actual business behavior clearly.
A balanced Rails suite uses isolated unit tests with focused doubles, integration tests for framework behavior, and a small number of carefully controlled external-service tests to validate the complete system.
32
What is the recommended approach for testing the endpoints of a Rails API?
KnownRevise
Test a Rails API primarily with integration tests that send requests through routing, controllers, and Active Record, then verify the response, side effects, and error handling. Rails 8 uses Minitest by default, with ActionDispatch::IntegrationTest providing full-stack request testing.
Read full answerHide full answer
Applies toRails 8 with the default Minitest test framework
Testing Strategy
Use several testing layers rather than relying exclusively on endpoint tests:
Model tests: Verify validations, scopes, associations, and model-level business rules.
Integration tests: Exercise the complete request path, including URL routing, controllers, strong parameters, serialization, and database changes. These are the primary tests for an API contract.
Service or job tests: Test business operations and background jobs in isolation when they contain meaningful logic.
External-service tests: Stub HTTP calls and other network dependencies. Tests should not depend on live third-party services.
ActionDispatch::IntegrationTest is generally more valuable than a narrowly scoped controller test because it confirms that the route, controller, model, and response work together. Minitest’s request helpers such as get, post, patch, and delete should be used with normal parameter hashes and as: :json rather than manually constructed JSON strings.
What to Assert
A good API test verifies only the behavior promised by the endpoint:
Scenario
Typical assertions
Successful request
Correct status, response headers, JSON structure, and database side effect
Invalid input
422 Unprocessable Entity and field-level validation errors
Missing or malformed input
Appropriate 400 Bad Request response
Unauthenticated request
401 Unauthorized
Unauthorized action
403 Forbidden
Missing resource
404 Not Found
Avoid asserting an entire JSON object when fields may be added later. Validate required fields and important values instead. For create and update endpoints, also verify that valid records produce the expected database changes and that invalid records do not.
Best Practices and Common Pitfalls
Keep strong parameters in controllers and business validation in models.
Test authentication and authorization separately, including attempts to access another user’s resource.
Use includes, preload, or eager_load when endpoints retrieve associations, and guard against N+1 queries with query-count assertions where appropriate.
Rails tests normally use transactional cleanup, but external effects such as emails, jobs, and webhooks may require explicit cleanup or stubbing.
Keep secrets and production credentials out of tests; use dedicated test credentials and environment-specific configuration.
Run the relevant Minitest files with bin/rails test, using the generated test directory as the normal location for API integration tests.
33
How are system tests defined and what steps are involved in adding them to a Rails application?
KnownRevise
System tests are end-to-end tests that exercise a Rails application through its user interface, including routing, controllers, views, JavaScript, and user interactions. Rails 8’s default setup uses Capybara with Selenium WebDriver and headless Chrome; system testing was introduced in Rails 5.1.
Read full answerHide full answer
Applies toRails 8 with Capybara 3.x and Selenium WebDriver 4.x
What System Tests Cover
Full-stack behavior: A system test starts at the user interface, submits forms, follows links, and checks visible results instead of calling controllers or models directly.
JavaScript and Hotwire: A JavaScript-capable driver can verify Turbo navigation, Turbo Stream updates, and Stimulus behavior that request-based integration tests cannot fully exercise.
Performance trade-off: Browser-driven tests are slower than unit or integration tests, so they should focus on critical journeys such as authentication, checkout, or important Turbo interactions.
Aspect
System test
Integration test
Browser
Real browser by default
No real browser by default
Scope
Full UI flow
Request/response flow
Speed
Slower
Faster
Best use
Critical end-to-end journeys
Routing, controller, and response logic
Steps to Add System Tests
Install dependencies: A newly generated Rails 8 application already includes capybara and selenium-webdriver in its test group. If either is absent, add it to that group and run bundle install.
Configure the driver: Rails generates test/application_system_test_case.rb. ApplicationSystemTestCase inherits from ActionDispatch::SystemTestCase and uses driven_by to select and configure the browser driver.
Generate a test file: Run bin/rails generate system_test articles to create test/system/articles_test.rb.
Write the user journey: Use Capybara’s DSL, including visit, fill_in, click_on, within, assert_text, and assert_selector.
Prepare the browser and run the tests: Install Chrome on the test machine or CI agent, then run bin/rails test:system. bin/rails test does not run system tests by default, while bin/rails test:all includes them.
Code Example
The following example creates the resource used by the test so that the routes, form, and database table all exist. It assumes default English scaffold labels.
# Create the resource and its table, then generate the system test.
bin/rails generate scaffold Article title:string body:text
bin/rails db:migrate
bin/rails generate system_test articles
Rails 8 generates the following base class:
# test/application_system_test_case.rb
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# Headless Chrome is suitable for CI; use :chrome to watch it locally.
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
end
Replace the generated test with a user journey:
# test/system/articles_test.rb
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
test "visitor creates an article" do
visit new_article_path
fill_in "Title", with: "System tests"
fill_in "Body", with: "Exercise the whole stack."
click_on "Create Article"
# Capybara waits for matching content to appear.
assert_text "Article was successfully created."
assert_text "System tests"
end
end
Best Practices and Common Pitfalls
Test important journeys: Keep system tests focused on critical workflows; cover detailed validation behavior in model tests and request/response behavior in integration tests.
Avoid sleep: Use waiting assertions such as assert_text and assert_selector. Use assert_current_path to verify the resulting location.
Scope queries: Use within when identical labels or text appear in multiple parts of the page.
Keep test data isolated: Rails system tests use transactional tests by default; create and roll back records deliberately rather than sharing state between examples.
Prefer stable selectors: Accessible labels and visible text are generally more durable than CSS selectors tied to page structure.
Capture failure evidence: Capybara’s save_screenshot can save an image for diagnosing failures.
Match the CI browser: The generated Chrome setup can run on Linux, macOS, and Windows, but the CI image must provide the required Chrome installation.
34
In Rails, what mechanism protects an application against Cross-Site Request Forgery, and how does that protection work?
KnownRevise
CSRF protection in Rails uses a session-bound authenticity token, together with a same-origin check, to reject forged state-changing browser requests. In Rails 8’s default ActionController::Base setup, the token is verified before the action and the same-origin check runs after it.
Read full answerHide full answer
Applies toRails 8
How Verification Works
Default protection: Rails 8 enables protect_from_forgery with: :exception by default for ActionController::Base; the default exception strategy was introduced in Rails 5.2. protect_from_forgery adds verify_authenticity_token as a before_action and verify_same_origin_request as an after-action check.
Safe methods: verified_request? accepts GET and HEAD without a token. Other HTTP verbs handled by a protected controller require an authenticity token.
Token sources: Rails accepts the token from the authenticity_token request parameter or the X-CSRF-Token request header.
Masked tokens: The session contains a random real token in session[:_csrf_token]. Rails renders a masked form containing that token and a newly generated one-time pad. The server unmasks the submitted value and compares it with the session token, so the value rendered in each response differs even though the underlying session token is stable.
Origin check: valid_request_origin? accepts a missing Origin header. When the header is present on an unsafe request, it must match request.base_url. This check is enabled by default through config.action_controller.forgery_protection_origin_check.
Failure handling: An invalid or missing token causes the configured strategy to run. The default raises ActionController::InvalidAuthenticityToken; an invalid origin can also cause the separate after-action check to raise ActionController::UnsafeRequestError if the token failure was handled without an exception.
Getting Tokens Into Pages
csrf_meta_tags: Placing this helper in the application layout’s <head> emits the csrf-param and csrf-token meta tags. Turbo, the Rails 8 default, uses the token when making applicable requests.
form_with: Rails forms created with form_with automatically include a hidden authenticity_token field for non-GET submissions, so ordinary views require no manual token field.
Per-form tokens: Rails 8 enables config.action_controller.per_form_csrf_tokens by default. A per-form token is scoped to the form’s action path and HTTP method, limiting its usefulness against a different endpoint.
Custom requests: form_authenticity_token is available in controllers and views when a custom integration needs a masked authenticity token. It does not expose the unmasked session token.
Strategies and Controller Scope
Strategy
Token-failure behavior
:exception
Raises ActionController::InvalidAuthenticityToken; this is the Rails 8 default.
:null_session
Replaces the session with an empty one and continues without raising a token-failure exception.
:reset_session
Resets the session and continues without raising a token-failure exception.
Per-controller behavior: protect_from_forgery with: :null_session or with: :reset_session selects a different token-failure strategy.
Skipping narrowly: skip_forgery_protection, introduced in Rails 5.2, skips the token callback for selected actions. Use it only when another mechanism, such as verified HMAC signatures for webhooks, protects the endpoint.
Callback order: protect_from_forgery prepend: true places the verification callback before previously declared callbacks.
API controllers: ActionController::API does not include Rails’ request-forgery protection. Such applications commonly use non-cookie authentication; if an API controller accepts browser cookies, equivalent CSRF protection must be added.
Best Practices and Common Pitfalls
Keep cookies enabled: Rails has defaulted generated cookies, including the session cookie, to SameSite=Lax since Rails 6.1. Most modern browsers therefore omit the session cookie on cross-site form POSTs, although the request itself can still reach Rails. This is defense in depth, not a replacement for authenticity tokens.
Never mutate state with GET: GET and HEAD bypass token verification, so destructive operations must use verbs such as POST, PATCH, or DELETE.
Skip narrowly: Always scope skipped actions and require a signature, API token, or equivalent independent verification.
Keep authentication and authorization: CSRF protection is not authorization. It binds a request to a session possessing a valid token; a leaked token may still be replayed.
Authentication generator: Rails 8 introduced bin/rails generate authentication, whose session-based forms obtain the token automatically through form_with.
Tests: Rails-generated config/environments/test.rb sets config.action_controller.allow_forgery_protection = false, so integration tests do not normally need to submit tokens. This should not be treated as evidence that the production token flow works.
35
Which security weaknesses show up most often in web applications, and what does Rails do to protect against them?
KnownRevise
There is no universal frequency ranking, but the recurring web-application weaknesses are injection, cross-site scripting (XSS), cross-site request forgery (CSRF), broken authentication and access control, mass assignment, unsafe deserialization, open redirects, sensitive-data exposure, and missing browser or transport hardening. Rails 8 protects against many of these through secure defaults, but application code and production configuration must close the remaining gaps.
Read full answerHide full answer
Applies toRails 8.0
Injection and Command Execution
SQL injection: Active Record normally binds values used in hash conditions or placeholder queries, so user input is not parsed as SQL. Unsafe interpolation can still occur in fragments passed to methods such as find_by_sql, select, or order; use bound parameters or a strict allowlist for sortable columns.
LIKE searches: escape user-supplied wildcard characters with ActiveRecord::Base.sanitize_sql_like before adding the surrounding % characters.
Shell injection: Rails does not automatically make operating-system commands safe. Pass arguments separately to Ruby process APIs rather than constructing a shell command string.
Untrusted code: Rails does not make arbitrary deserialization safe. The cookie serializer has defaulted to JSON since Rails 7.0; do not restore the insecure legacy Marshal format.
Browser and Session Attacks
XSS: ERB escapes ordinary <%= ... %> output by default. raw and html_safe bypass that protection and must never be used on untrusted input; sanitize is a separate allowlist-based mechanism for approved rich-text tags and attributes.
CSP: Rails provides a CSP initializer and per-request nonce helpers, but upgrading to Rails does not enable a policy by itself. A restrictive script-src policy can use nonces to permit approved scripts in modern browsers.
CSRF: CSRF protection is enabled by default for full-stack ActionController::Base controllers. Since Rails 5.2, invalid tokens raise ActionController::InvalidAuthenticityToken by default. form_with includes an authenticity token, and Turbo submits it through the X-CSRF-Token header when the CSRF meta tags are present in the layout.
API controllers: ActionController::API does not provide the same CSRF protection. Cookie-authenticated API endpoints therefore need an explicit CSRF strategy; token-based APIs do not rely on ambient browser credentials.
Session fixation: call reset_session after successful authentication before storing the authenticated user identifier.
Cookies: Rails’s cookie session store encrypts and authenticates its payload. Use cookies.encrypted or cookies.signed for other cookies, and set secure: true and httponly: true. Rails 6.1 introduced the SameSite=Lax default.
Access Control, Authentication, and Sensitive Data
Mass assignment: use strong parameters and explicitly permit attributes. The older attr_accessible approach belongs to the deprecated protected_attributes gem; Rails 4 and later use strong parameters.
Passwords: has_secure_password uses bcrypt and stores a password_digest, not the plaintext password. Rails 8 introduced bin/rails generate authentication, which scaffolds a sign-in and password-reset flow, but applications must still add appropriate rate limiting, monitoring, and account-recovery controls.
Authorization: authentication proves who a user is; it does not prevent one user from accessing another user’s records. Every protected action needs an explicit authorization check, and resources should not be found solely from untrusted IDs.
Encryption: encrypts with Active Record Encryption, introduced in Rails 7.0, can encrypt sensitive columns. It requires configured encryption keys, which belong in Rails credentials or environment secrets rather than source code.
Token comparison: compare security-sensitive tokens with ActiveSupport::SecurityUtils.secure_compare, not ordinary equality.
Logging: extend config.filter_parameters so passwords, tokens, and other sensitive values are filtered from logs.
Rails Hardening and Deployment Boundaries
Risk
Rails 8 mechanism or requirement
Clickjacking
Rails sends X-Frame-Options: SAMEORIGIN by default.
MIME sniffing
Rails sends X-Content-Type-Options: nosniff by default.
Open redirects
redirect_to rejects external hosts unless allow_other_host: true is explicitly supplied.
Host-header attacks
config.hosts and ActionDispatch::HostAuthorization provide host allowlisting; host authorization is enabled by default in development and should be configured deliberately for other environments.
Brute-force requests
Rails 8 adds rate_limit controller support, backed by the configured cache store; production deployments need a suitable shared cache setup.
Plaintext transport
config.force_ssl = true redirects HTTP requests to HTTPS, and Rails’s SSL middleware can emit HSTS for clients that support it.
Sensitive headers and pages
CSP, frame protection, HSTS, host allowlists, and HTTPS must be configured and verified in production rather than assumed from development defaults.
Practical Priorities
Never render untrusted HTML with raw or html_safe, and never pass unpermitted parameters directly to mass-assignment APIs.
Add explicit authorization checks to every endpoint that accesses user-owned or administrative data.
Keep dependencies patched and run Brakeman in CI. If the application uses importmap-rails, also run bin/importmap audit.
Store credentials and encryption keys outside source control, filter sensitive parameters from logs, and use HTTPS in every non-local environment.
Verify CSP, force_ssl, config.hosts, cookie attributes, rate limiting, and the production cache configuration as part of deployment security checks.
36
How is the Devise gem used to implement authentication in a Ruby on Rails application?
KnownRevise
The Devise gem provides a configurable authentication framework for Rails applications, including user registration, credential verification, password recovery, sessions, and optional account-security features. It handles authentication, but application developers must still implement authorization rules that determine what each signed-in user can access.
Read full answerHide full answer
Applies toRails 8 with Devise 4.9.4
How Devise Works
Devise organizes authentication into two main components:
Models: The User model declares which authentication modules are enabled through devise.
Controllers and routes: devise_for :users generates sign-in, sign-out, registration, and password-recovery routes and controllers.
During a typical sign-in:
The user submits an email address and password.
Devise’s database_authenticatable module verifies the credentials.
The authenticated user identifier is stored in the Rails session.
Subsequent requests can access current_user.
Controllers use authenticate_user! to require authentication.
Devise uses the Rack-based Warden framework internally for authentication strategies and session management.
The user generator creates a model containing the selected Devise modules and a migration for the required database columns.
Configure important defaults in config/initializers/devise.rb:
Devise.setup do |config|
# Replace this address with a monitored sender in production.
config.mailer_sender = "accounts@example.com"
config.password_length = 12..128
config.reconfirmable = true
end
Devise creates routes such as /users/sign_in, /users/sign_up, and /users/password. Add the route and require authentication in a protected controller:
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
resource :dashboard, only: [:show]
root "dashboard#show"
end
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
before_action :authenticate_user!
def show
head :ok
end
end
Strong Parameters and Common Helpers
Devise exposes helpers for checking and accessing the authenticated user:
user_signed_in?: Returns whether a user is authenticated.
current_user: Returns the authenticated User or nil.
authenticate_user!: Redirects unauthenticated visitors to the sign-in page.
new_user_session_path: Path for the sign-in form.
new_user_registration_path: Path for the sign-up form.
destroy_user_session_path: Path used to sign out.
A Turbo-compatible ERB view can use these helpers:
Devise already permits a default set of keys for sign-up and sign-in (:email, :password, :password_confirmation); any extra attribute must be permitted explicitly through Devise’s parameter sanitizer:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
# :name requires a matching column on users; the default keys stay permitted.
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
end
end
Available Authentication Modules
Devise features are enabled selectively on the model:
omniauthable: Supports external authentication through OmniAuth.
Only enabled modules generate their corresponding routes, helpers, and behavior. Devise stores password digests rather than plaintext passwords, but it does not automatically impose password complexity rules beyond the configured length.
Security and Authorization Boundaries
Authentication is not authorization: After authenticate_user!, the application must still verify that the user may access a specific record or action.
Do not permit privileged attributes: Avoid permitting fields such as role, admin, or account_id unless users are explicitly allowed to set them.
Protect every protected endpoint: Apply authenticate_user! consistently instead of relying on hidden links or controller-level conventions.
Keep normal Rails protections: Use Rails’ CSRF protection, HTTPS, secure cookies, and production-safe session configuration.
Configure email delivery properly: Password recovery and confirmation depend on a functioning mailer and an approved sender address.
Avoid duplicate authentication systems: Rails 8 introduced bin/rails generate authentication for basic built-in authentication. It is an alternative to Devise, not something that must be installed alongside Devise.
Devise is most appropriate when an application needs several standard authentication features and a consistent user model. For a small application needing only sign-in and sign-out, Rails 8’s built-in generator may require fewer dependencies.
37
What causes SQL injection, and how does Rails 8 protect database queries against it?
KnownRevise
SQL injection occurs when untrusted input becomes part of a SQL statement’s executable syntax instead of being handled as data. Rails 8 primarily prevents this through Active Record’s parameterized queries, which keep SQL structure separate from input values.
Read full answerHide full answer
Applies toRuby on Rails 8 with Active Record
What Causes SQL Injection
An injection vulnerability usually begins when application code interpolates a request parameter or other untrusted value directly into a SQL fragment. For example, interpolating a name into a where condition can allow an attacker to add predicates such as OR '1'='1', changing the query’s meaning and potentially exposing unauthorized records.
SQL injection can affect any part of a statement, including:
where, order, group, and having fragments
Raw SQL executed through Active Record
Dynamically selected table or column names
Any interpolated application-controlled text that Active Record interprets as SQL
The defining problem is not merely that input contains SQL-like characters; it is that the application fails to distinguish trusted SQL structure from untrusted data.
How Rails 8 Protects Database Queries
Active Record’s query methods separate SQL templates from values. A placeholder query such as where("name = ?", value) treats value as a parameter rather than SQL syntax. Hash conditions such as where(name: value) are also safe because Active Record generates the SQL structure and safely quotes or binds the value according to the database adapter.
This protection depends on continuing to use the query API correctly. Interpolating a value into a string—where("name = '#{value}'")—removes that separation and creates an injection risk.
Other Rails features serve different purposes:
Strong parameters prevent mass assignment; they do not directly prevent SQL injection. params.expect, introduced in Rails 8, returns permitted parameters for a known shape, while the classic form is params.require(:user).permit(:name, :email).
Active Record validations enforce model and business rules rather than SQL safety.
Callbacks run application logic around persistence operations and should not be treated as sanitization.
Database privileges limit the damage if an injection flaw or compromised query nevertheless occurs.
Unsafe Construction and Dynamic Identifiers
If Active Record accepts an interpolated string as a raw SQL fragment, it generally cannot infer which characters were intended as data. Consequently, passing user input directly as an ordering expression, grouping expression, raw condition, or complete SQL statement is unsafe.
Values and identifiers also require different handling:
Input kind
Safe approach
Data value
Use a hash condition or placeholder parameter
Column, table, or direction
Map an allowlisted external value to a predetermined identifier
Ordinary bind parameters cannot represent arbitrary SQL identifiers because identifiers are part of SQL grammar. Dynamic table or column selection should therefore use a fixed allowlist and then use the selected identifier through Active Record’s normal APIs.
For genuinely necessary raw SQL, use the adapter’s supported parameter-binding mechanism rather than embedding values manually. Placeholder support can differ among PostgreSQL, MySQL, and SQLite adapters, so adapter-independent Active Record query methods are preferable.
Best Practices and Common Pitfalls
Prefer hash conditions such as where(name: value) or parameterized fragments such as where("name = ?", value).
Never interpolate request parameters, headers, cookies, or other untrusted input into SQL fragments.
Treat dynamic identifiers separately and map them through an allowlist.
Do not mistake strong parameters, validations, or callbacks for SQL-injection defenses.
Avoid relying on escaping or sanitization when ordinary parameterization is available.
Give the application database account only the tables and operations it requires.
Test security-sensitive queries with malicious inputs and review raw SQL and logs for accidental interpolation.
38
What are the main techniques for improving the performance of a Ruby on Rails application?
KnownRevise
Performance optimization in Rails is most effective when driven by measurements rather than guesswork. The largest gains usually come from eliminating N+1 queries, adding appropriate database indexes, reducing unnecessary work in requests, and caching expensive computations.
Read full answerHide full answer
Applies toRails 8
Finding the Bottleneck
Use profiling and monitoring to identify where time is spent before changing the application:
Request timing: Tools such as ActiveSupport::Notifications, Rails instrumentation, or an APM service can reveal slow controllers, views, and database calls.
Development logs: Pay attention to repeated SQL queries, especially many nearly identical queries during one request.
Production metrics: Track response time, throughput, CPU, memory, database utilization, background-job latency, and error rates.
Browser tools: Use browser developer tools to inspect network requests, transferred payloads, rendering time, and duplicated assets.
Database analysis: PostgreSQL or MySQL query plans can show whether a query performs a sequential scan, uses an unsuitable join, or sorts more rows than expected.
Optimize the slowest measured path first. A micro-optimization in Ruby often has less impact than avoiding one unnecessary query per row.
Database and Active Record Optimizations
Prevent N+1 queries: When a view accesses associations for many records, load those associations in advance with includes, preload, or eager_load.
preload runs separate queries and is usually preferable for simple association loading.
eager_load uses joins and is useful when conditions reference tables associated through has_many or has_one.
includes lets Active Record choose an appropriate strategy based on the query.
Enable strict_loading: When enabled for a model or association, strict loading raises an exception if code attempts to lazy-load it, helping expose N+1 problems during development.
Add targeted indexes: Index columns used for filtering, sorting, joining, and uniqueness checks. Composite index order should reflect the query’s predicates and sort requirements.
Reduce selected data: Use select to retrieve only required columns, while retaining any primary key and columns needed by Active Record.
Avoid unbounded result sets: Use pagination or keyset pagination rather than loading an entire table.
Use efficient aggregates: count, exists?, and pluck can avoid loading complete Active Record objects when only one value is needed.
Maintain counters: A properly configured counter_cache can avoid repeated count queries for belongs_to associations.
Batch writes: Prefer bulk operations or appropriately sized batches over issuing one INSERT or UPDATE per object.
Application-Level Optimizations
Cache repeated work: Rails caching can store fragments, query results, or other computed values. Solid Cache became the default production cache store for newly generated applications in Rails 8. Cache keys should include every input that can change the result, and expiration or invalidation should match the data’s freshness requirements.
Move slow work out of requests: Send email delivery, file processing, and other non-interactive work to background jobs. Solid Queue became the default Active Job backend for newly generated applications in Rails 8.
Optimize serialization and rendering: Limit fields returned by APIs, avoid unnecessary object instantiation, and keep view work simple.
Compress and optimize assets: New Rails 8 applications use Propshaft for assets and importmap-rails for JavaScript by default. A JavaScript bundler can be selected instead when a project needs one.
Use efficient HTTP behavior: Set appropriate cache headers, use ETags, and serve static files through the production web server or CDN.
Scale deliberately: Database connection limits, web-process concurrency, and job-worker capacity must be coordinated. More web processes cannot compensate for an exhausted database pool or inefficient SQL.
Best Practices and Common Pitfalls
Profile in an environment representative of production; development timing can be misleading.
Check the actual SQL and query plan rather than assuming includes always issues one query.
Use eager_load only when joins are necessary; otherwise preload often produces simpler SQL.
Do not cache responses containing user-specific or sensitive data without properly namespacing and expiring the keys.
Avoid selecting a large number of columns or returning unbounded JSON arrays.
Test performance with realistic data volumes. An index may not help when most table rows match the predicate.
Do not use find_by_sql with interpolated input. Bind values or use Active Record’s hash conditions.
After each change, compare latency, query count, CPU, memory, and database load to verify that the optimization provides a real benefit.
39
What process do you follow to locate and diagnose performance bottlenecks in a Rails application?
KnownRevise
Identify bottlenecks by combining profiling and observability with controlled load testing and realistic data volumes. Measure each layer first, then use the results to target database, application, network, or infrastructure improvements rather than guessing.
Read full answerHide full answer
Applies toRuby on Rails 8
Establish a Reliable Baseline
Reproduce the slow operation with a representative dataset, request parameters, and concurrency level.
Record latency distributions such as p50, p95, and p99 instead of relying on a single timing.
Separate cold-cache behavior from steady-state behavior; cached and uncached paths may have different bottlenecks.
Test against production-like data sizes because query plans, payload sizes, and memory usage can change substantially as tables grow.
Identify the actual symptom: slow page responses, high database CPU, excessive memory, queued-job delays, or poor throughput under concurrency.
Instrument the Request Path
Signal
What it reveals
What to investigate
Browser Network panel
Transfer size, connection setup, TTFB, and client rendering time
Slow TTFB, large assets, excessive requests, or backend latency
process_action.action_controller
Total controller processing time, reported database time, and reported view time
Slow actions, queries, or template rendering
sql.active_record
Duration and operation name for individual Active Record queries
N+1 queries, missing indexes, excessive calls, or expensive SQL
render_template.action_view
Time associated with template rendering
Inefficient templates, expensive helpers, or missing fragment caching
Production APM traces
End-to-end spans across Rails, databases, queues, and external services
Cross-service latency and hidden downstream dependencies
Rails exposes these signals through ActiveSupport::Notifications. Browser developer tools complement them by showing whether time is actually spent in the backend or in asset delivery and browser processing.
Diagnose Database Problems
N+1 queries: Look for the same query operation repeated many times during one request. Use includes or preload when associated records will be accessed separately; use eager_load when the query must join and filter on the associated table.
Slow query plans: Inspect the database’s query plan and consider indexes on columns used in filters, joins, and ordering. Validate composite-index column order against actual predicates.
High query volume: Determine whether repeated work can be cached, batched, or avoided by retrieving only required columns.
Database saturation: Check CPU, I/O, lock waits, connection-pool waits, and buffer-cache behavior. An apparently slow Active Record call may be waiting for a database connection rather than executing expensive SQL.
Unsafe diagnostics: EXPLAIN ANALYZE executes the statement, so run it carefully and primarily in staging or against a safe replica.
Code Example
The following development-only subscriber reports request and query timings without logging SQL parameters or potentially sensitive values.
# config/initializers/performance_logging.rb
if Rails.env.development?
ActiveSupport::Notifications.subscribe(
"process_action.action_controller"
) do |_name, start, finish, _id, payload|
total_ms = ((finish - start) * 1000).round(1)
# db_runtime and view_runtime are already reported in milliseconds
db_ms = (payload[:db_runtime] || 0).round(1)
view_ms = (payload[:view_runtime] || 0).round(1)
Rails.logger.info(
"[perf] controller=#{payload[:controller]} " \
"action=#{payload[:action]} total_ms=#{total_ms} " \
"db_ms=#{db_ms} view_ms=#{view_ms} status=#{payload[:status]}"
)
end
ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, start, finish, _id, payload|
duration_ms = ((finish - start) * 1000).round(1)
Rails.logger.debug(
"[sql] operation=#{payload[:name]} " \
"duration_ms=#{duration_ms} cached=#{payload[:cached]}"
)
end
end
Repeated query operations with short individual durations can reveal an N+1 pattern even when no single query appears extremely slow.
Prioritize and Verify Fixes
Rank findings by user impact, request frequency, and tail latency. A frequently executed 50 ms query may matter more than a rare one-second query.
Address the largest layer first: optimize SQL and indexes, remove repeated work, reduce transferred data, or move non-user-facing processing to background jobs.
In Rails 8’s default production setup, Solid Queue can process background work, but jobs should have clear timeouts, retry policies, and idempotency where appropriate.
Re-run the same load test after each change and compare p95 and p99 latency, throughput, error rate, database load, and memory consumption.
Add regression tests or performance thresholds where practical, and document expected cache behavior and data-volume assumptions.
Common mistakes include optimizing from one development-machine request, adding indexes without examining query plans, caching personalized responses incorrectly, logging sensitive query parameters, and declaring success without repeating the workload under concurrency.
40
How does caching work in Rails, and which caching strategies are available?
KnownRevise
In Rails, caching means storing the result of expensive work—rendered HTML, database query results, or computed Ruby objects—so later requests can reuse it instead of recalculating it. Rails exposes a cache-store abstraction plus helpers at several layers: HTTP responses, views, application code, and Active Record.
Read full answerHide full answer
Applies toRails 8
Cache Stores
Store abstraction: All caching goes through an ActiveSupport::Cache::Store, configured with config.cache_store. Stores implement read, write, fetch, delete, and related operations.
Solid Cache: Rails 8 new applications default to :solid_cache_store in production. It stores cached entries in the database using the Solid Cache gem.
Development and test: Development commonly uses :memory_store when caching is enabled with rails dev:cache; otherwise it uses :null_store. Test environments typically use :null_store.
Other stores: :memory_store is process-local and not shared across processes. :file_store uses disk files. :redis_cache_store requires the redis gem. :mem_cache_store requires the dalli gem. :null_store discards writes and is useful in tests.
HTTP and Conditional Caching
Response headers: expires_in sets Cache-Control: max-age and marks the response private by default, so only the browser may reuse it; pass public: true when a CDN or other shared cache should be allowed to serve it without hitting Rails.
Conditional GET: fresh_when and stale? compare the ETag or Last-Modified timestamp the client sends back in If-None-Match or If-Modified-Since with the current representation and return 304 Not Modified when appropriate.
ETag source: Use stable versioned keys, such as a record’s cache_key_with_version, so the ETag changes when the underlying data changes.
Best fit: Public, cacheable responses such as product pages, feeds, and API representations. Avoid shared HTTP caching for personalized or sensitive responses unless the cache key and headers make that safe.
View and Fragment Caching
Fragment caching: The cache helper in views stores a rendered fragment in the configured cache store. cache_if and cache_unless apply conditions.
Russian doll caching: An outer fragment can contain cached inner fragments. When a child record changes, touch: true on the association updates the parent’s timestamp and invalidates the parent fragment.
Collection caching: Rendering a collection with cached: true reuses per-item fragments while still rendering the collection wrapper.
Invalidation: Fragment keys normally include model cache keys and versions, so updates change the key. Time-based expiry is also common.
Application-Level and Query Caching
Low-level caching: Rails.cache.fetch(key, expires_in: ...) { ... } reads a value or computes and writes it. fetch_multi, write_multi, read, write, and delete support bulk and explicit operations.
Use cases: Expensive API calls, computed results, reports, and serialized objects that are costly to rebuild.
Query caching: Active Record automatically caches identical SELECT results for the duration of a request and clears the query cache after writes. It is per-request and not a shared cache, so it does not replace Rails.cache.
Key design: Include tenant, locale, user scope, and a version component in keys when the cached value depends on them.
Legacy Page and Action Caching
Page caching: Wrote fully rendered HTML into public, bypassing Rails entirely. It was removed from Rails core in Rails 4 and is available only through the legacy actionpack-page_caching gem.
Action caching: Cached a controller action’s response while still running filters. It was also removed from core in Rails 4 and lives in the legacy actionpack-action_caching gem.
Replacement: For new Rails 8 applications, use HTTP caching with CDNs and expires_in/fresh_when, fragment caching for views, and low-level caching for computed values.
Comparison and Best Practices
Strategy
What is cached
Scope
Typical invalidation
HTTP/CDN
Full response
Browser or proxy
Cache-Control, ETag, expiry
Fragment
Part of a view
Cache store
Key version, touch, expiry
Low-level
Arbitrary Ruby value
Cache store
Explicit key, TTL, delete
Query
SELECT results
Single request
Automatic after writes
Page/action
Full response
Legacy gems
Manual or expiry
Cache selectively: Cache data that is expensive and reused; measure first.
Version keys: Prefer model cache keys and explicit version segments over manual string keys.
Bound growth: Set expires_in and avoid relying on broad key deletion, which is expensive or unsupported in some stores.
Protect private data: Never share a fragment or HTTP response across users unless the key and headers include the user-specific scope.
Test invalidation: Verify that updates, deletes, and association changes expire the right entries.
41
Why does Rails provide eager loading, and in what ways can it be applied?
KnownRevise
Eager loading loads Active Record associations in advance so the application can access them without issuing one additional query per record—the N+1 query problem. It reduces database round trips by batching related data before it is used.
Read full answerHide full answer
Applies toRuby on Rails 8 (Active Record 8.0)
Why It Exists
Lazy loading by default: Active Record queries an association when it is first accessed. Iterating over N parents and accessing one association can therefore produce 1 query for the parents plus N association queries.
Database overhead: every round trip adds latency, query planning, and object-instantiation costs. The impact can be much greater than the raw SQL execution time.
Batched access: eager loading fetches the requested associations ahead of use and populates their in-memory targets, so later access does not trigger more queries.
Predictable query shape: with eager loading, the query count generally depends on the association structure rather than the number of parent records.
Choosing a Method
Method
How associations are fetched
Association loaded?
Conditions or ordering on the associated table
includes
Usually separate preload queries; Rails can use joined eager loading when the associated table is referenced
Yes
Supported; hash conditions are recognized automatically, while string conditions generally require references
preload
Separate queries for each association scope, without joining it to the main query
Yes
Not supported in the main query, but an association scope can filter or sort the loaded records
eager_load
One joined query using one or more LEFT OUTER JOIN clauses
Yes
Supported; not supported for polymorphic associations
joins
An INNER JOIN
No
Supported for filtering or ordering the main result
includes: the usual first choice. where(posts: { published: true }) causes Rails to recognize the referenced table. A string condition such as order("posts.created_at DESC") should be accompanied by references(:posts).
preload: keeps association queries separate and predictable. A scoped preload can filter the associated records, but it cannot filter or order the parent query using columns from the associated table.
eager_load: is appropriate when the main query must filter or order by associated columns. A has_many join can produce multiple joined rows per parent, so .distinct is often needed when each parent should appear once.
joins: can filter, order, or select data from the joined table, but it does not populate the association. Accessing that association afterward can still cause N+1 queries.
Applying It to Associations
Load several associations with includes(:posts, :comments).
Load nested associations with includes(posts: :author) or includes(posts: [:author, :tags]).
includes and preload support has_many :through associations; preload is the appropriate choice for polymorphic associations.
Eager loading affects only the associations named in the relation. Other associations remain lazy unless they are included explicitly.
strict_loading is complementary rather than an eager-loading method: it raises ActiveRecord::StrictLoadingViolationError when an unloaded association is accessed on a strict-loading owner.
Best Practices and Common Pitfalls
Apply eager loading where the relation is constructed, commonly in a controller, service object, or scope, so views do not trigger hidden queries.
Avoid loading unbounded has_many collections. Large joins can create very large result sets, and a scoped or paginated preload is often safer.
Be especially careful with eager_load and limit or offset: the limit applies to the joined rows rather than necessarily to distinct parents. Prefer preload when parent pagination must remain exact.
Call size on an already loaded collection to use its in-memory size; count performs a database query.
Strict loading was introduced in Rails 6.1. Rails 7.0 added association-level strict_loading: true and the config.active_record.strict_loading_by_default setting. Use strict loading selectively in development or CI to expose unintended lazy loads; the third-party Bullet gem can provide broader N+1 detection.
42
What is the process for using the Bullet gem to detect N+1 query issues in a Rails application?
KnownRevise
The Bullet gem detects N+1 query problems by monitoring Active Record query execution and association loading, then warning or raising when a query runs repeatedly in a loop without eager loading. It is normally enabled in development and test environments so issues are caught before production.
Read full answerHide full answer
Applies toBullet 7.x with Rails 8
How It Works
Bullet monitors Active Record query execution through Active Support instrumentation and tracks which associations are loaded and which queries are executed.
When an association is accessed in a loop and triggers a separate query per iteration, Bullet flags it as an N+1 problem.
It also detects unused eager loading, where associations are preloaded but never actually accessed.
Warnings go to the configured logger; in test environments, they can be raised as exceptions.
Setup and Configuration
Add the gem to your Gemfile in the :development and :test groups.
Create config/initializers/bullet.rb.
Enable logging and, for tests, error raising. Guard the initializer with defined?(Bullet) so production does not fail when the gem is not loaded.
Code Example
# Gemfile
group :development, :test do
gem "bullet"
end
# config/initializers/bullet.rb
if defined?(Bullet)
Bullet.enable = true
Bullet.bullet_logger = true # writes to log/bullet.log
Bullet.rails_logger = true # also writes to the Rails log
Bullet.raise = true if Rails.env.test? # fail tests when Bullet detects an issue
end
Best Practices
Enable Bullet in development so you see warnings while writing code.
Set Bullet.raise = true only in the test environment so N+1 queries fail tests.
Review log/bullet.log or the Rails log for the model names and association paths Bullet reports.
Fix N+1 problems with includes, preload, or eager_load in Active Record queries.
Do not enable Bullet in production; it adds overhead.
Common Pitfalls
Bullet can produce false positives with complex query patterns.
It only detects issues in code paths that are actually executed.
Use query counting in tests as a complement; do not rely on Bullet alone.
43
Which tools are commonly used to monitor the performance of a Rails application?
KnownRevise
Common choices are Application Performance Monitoring (APM) services such as New Relic, Datadog, AppSignal, and Skylight, plus open-source tools such as Bullet, Rack::MiniProfiler, and a Prometheus/Grafana stack.
Read full answerHide full answer
Applies toRuby on Rails 8
APM Services
New Relic: Provides deep visibility into request traces, database queries, and background job performance. It integrates with Rails via the newrelic_rpm gem and automatically instruments common frameworks.
Datadog: Offers APM, logs, and infrastructure monitoring. It uses the ddtrace gem for Ruby and provides distributed tracing across services.
AppSignal: Focuses on Ruby applications with automatic error tracking, performance metrics, and queue monitoring. It is lightweight and easy to configure.
Skylight: A Rails-focused APM service that highlights slow endpoints, SQL queries, and view rendering.
Open-Source Gems and Libraries
Bullet: Detects N+1 query problems and unnecessary eager loading in Active Record. It logs warnings in development and can be configured to raise errors in tests, helping you optimize database queries before they reach production.
Rack::MiniProfiler: Adds a performance profiling panel to the bottom of web pages, showing query times, view rendering, and memory usage. It is useful for development and staging.
Prometheus + Grafana: A self-hosted stack where you can export Rails metrics via the prometheus-client gem and visualize them with Grafana dashboards.
Built-in Instrumentation
ActiveSupport::Notifications: Rails includes a pub/sub system for instrumentation. You can subscribe to events like sql.active_record or process_action.action_controller to log custom metrics.
Query Log Tags: With config.active_record.query_log_tags_enabled = true, Rails can append tags configured in config.active_record.query_log_tags (for example :controller, :action, and :job) to SQL logs, making it easier to correlate queries with specific requests or jobs.
Log Level: Setting config.log_level = :debug in development provides detailed SQL and request logs for manual inspection.
Comparison of Tools
Tool
Type
Best For
New Relic
APM Service
Comprehensive tracing and alerts
Datadog
APM Service
Unified monitoring across infrastructure
AppSignal
APM Service
Ruby-specific metrics and error tracking
Skylight
APM Service
Rails-specific request and query profiling
Bullet
Gem
Detecting N+1 queries
Rack::MiniProfiler
Gem
Development and staging profiling
Best Practices
Monitor in Production: Use APM services in production to get real-user metrics and alerting.
Optimize Queries: Use Bullet in development to catch N+1 queries before they reach production.
Profile Regularly: Use Rack::MiniProfiler in staging to identify slow endpoints.
Set Up Alerts: Configure alerts for high response times or error rates in your APM dashboard.
Review Background Jobs: In Rails 8, Solid Queue is the default Active Job backend; monitor its performance via APM or job-specific dashboards to ensure background tasks do not bottleneck the application.
44
How can you create and manage database indexes in Rails 8, and what role do they play in application performance?
KnownRevise
Rails creates and manages database indexes through Active Record migrations, allowing PostgreSQL to find matching rows without scanning an entire table. They can substantially improve suitable query plans, but they also consume storage and add work to inserts, updates, and deletes.
Read full answerHide full answer
Applies toRuby on Rails 8 with PostgreSQL
How Rails Manages Indexes
The primary migration APIs are:
add_index: creates an index.
remove_index: removes an index.
rename_index: renames an index.
index_exists?: checks whether an index exists.
Migrations apply index changes through bin/rails db:migrate. With Rails’ default schema dumping, db/schema.rb records the resulting index definitions. If schema_format = :sql is configured, Rails instead maintains db/structure.sql.
db:prepare creates the database and loads the schema when necessary; otherwise, it runs pending migrations. It is not restricted to development.
For a new table, add_reference :orders, :customer, foreign_key: true creates customer_id, adds an index on that foreign-key column by default, and adds a foreign-key constraint to customers.id. If the column already exists, use add_foreign_key rather than running add_reference again. The referenced target must have a primary or unique key.
Index Strategies
Strategy
Purpose
Rails declaration
Single-column
Filters or joins on one column
add_index :orders, :status
Composite
Supports related predicates and ordering
add_index :orders, [:customer_id, :created_at]
Unique
Rejects duplicate values
add_index :users, :email, unique: true
Partial
Indexes only selected rows
add_index :orders, :status, where: "archived_at IS NULL"
Expression
Indexes a computed expression
add_index :users, "lower(email)", unique: true
Partial and expression indexes are database-specific. The examples here use PostgreSQL; MySQL does not support partial indexes, so equivalent applications must use another design.
For a composite index, column order matters. An index on [:customer_id, :created_at] can efficiently handle an equality predicate on customer_id followed by ordering on created_at. PostgreSQL B-tree indexes can also be scanned backward, allowing that index to support the corresponding descending order when other query conditions permit.
Role in Performance
Without a suitable index, PostgreSQL may perform a sequential scan that reads every row. With one, the query planner can use a more selective index scan, although it may still correctly choose a sequential scan for a small table or a query matching most rows.
Indexes are especially valuable for:
Frequently filtered columns.
Foreign-key columns used in joins.
Columns used to sort or group large result sets.
Columns that must enforce uniqueness.
Common account-scoped queries that retrieve recent records.
Indexes do not fix every performance problem. N+1 behavior is primarily a query-count issue: includes and preload are designed to prevent common N+1 patterns, while an index only makes each query that is issued faster. An index also cannot compensate for unnecessary columns, poor predicates, or stale statistics.
Code Example
Assuming orders already has customer_id, created_at, and archived_at, this migration creates several PostgreSQL indexes:
class AddPerformanceIndexesToOrders < ActiveRecord::Migration[8.0]
def change
# Supports filtering by customer and sorting by creation time.
add_index :orders, [:customer_id, :created_at],
name: "index_orders_on_customer_id_and_created_at"
# Enforces one order per order number at the database level.
add_index :orders, :order_number,
unique: true,
name: "index_orders_on_order_number"
# Indexes only orders that have not been archived.
add_index :orders, :status,
where: "archived_at IS NULL",
name: "index_active_orders_on_status"
end
end
Apply the migration with:
bin/rails db:migrate
The query Order.where(customer_id: 42).order(created_at: :desc).limit(20) may use the composite index. To remove it, use remove_index :orders, name: "index_orders_on_customer_id_and_created_at" in a later migration. rename_index :orders, "old_name", to: "new_name" renames an existing index.
For friendly validation errors, an Order model can use validates :order_number, uniqueness: true. The unique database index remains necessary because application-level validation cannot prevent concurrent duplicate writes by itself.
Best Practices and Common Pitfalls
Measure query plans: Use EXPLAIN or Rails’ explain method before adding an index. PostgreSQL’s EXPLAIN ANALYZE executes the statement, so use it carefully.
Avoid indexing every column: Unused indexes consume disk space and slow down writes that modify indexed columns.
Design composite indexes from real queries: Put selective equality predicates before ordering columns when appropriate.
Account for selectivity: A standalone index on a low-cardinality column may provide little benefit, although it can still help as part of a composite index.
Use statistics: PostgreSQL’s planner relies on table statistics; stale statistics can lead to poor index choices.
Verify indexes before removing them: Confirm production usage and consider dependencies such as foreign keys or constraints.
Handle large tables carefully: PostgreSQL supports concurrent index creation through Rails’ algorithm: :concurrently option in a dedicated migration with disable_ddl_transaction!; concurrent builds avoid blocking ordinary writes but require more work and operational care.
45
What is the way Rails manages a pool of database connections?
KnownRevise
In Rails, Active Record’s connection pool manages database connections: each Rails process owns pools for its configured database roles, leases connections to work that needs them, and returns them for reuse. Checkout, check-in, and idle-connection reaping happen automatically in normal request and job lifecycles.
Read full answerHide full answer
Applies toRails 8 (Active Record 8.x)
How the Pool Works
Process-local pools: Each database role or configuration has a separate pool within each Rails process. Replicas and databases configured through connects_to have their own pools. Pools are not shared between Puma workers, so three workers with a pool size of 5 can hold up to 15 connections.
Lease and reuse: A thread or fiber doing database work leases a connection from the relevant pool. The pool prevents two concurrent operations from using the same connection at the same time. After use, the connection is returned for another operation rather than closed.
Checkout and waiting: checkout uses an idle connection when one is available or creates another when the pool is below its configured size. If all connections are busy, the caller waits for up to checkout_timeout; the default is 5 seconds, after which Rails raises ActiveRecord::ConnectionTimeoutError.
Automatic cleanup: The Active Support executor returns connections to their pools at the end of requests and Active Job executions. Applications normally do not need to check connections in manually.
Lazy checkout: Introduced in Rails 7.2, lazy checkout means a connection is leased only when database work actually requires it. A request that never accesses the database does not consume a pool slot.
Idle reaping: Rails 7.2 also introduced idle-connection reaping. A reaper periodically closes connections idle longer than idle_timeout, whose default is 300 seconds.
Sizing the Pool
The generated config/database.yml sets pool from RAILS_MAX_THREADS, falling back to 5. Rails 8’s generated Puma configuration has a different fallback of 3 threads, so set RAILS_MAX_THREADS consistently when both defaults should match.
A pool should normally accommodate the peak number of threads that can access the database concurrently. Background workers and other processes have separate pools, so their connection usage must be counted separately.
Larger pools are not free: every connection consumes resources in both the Rails process and the database. For PostgreSQL, all application processes must also remain within the server’s max_connections limit.
Long transactions and slow queries retain connections for their full duration. Removing N+1 queries with includes, preload, or eager_load can therefore reduce pool contention indirectly.
Observing and Tuning
ActiveRecord::Base.connection_pool.stat: Reports pool information such as size, connections, busy, idle, dead, waiting, and checkout_timeout for the current process.
with_connection: Provides a connection outside a normal request or job lifecycle. A newly leased connection is checked back in when the block finishes, including when it raises; if the execution context already owns a connection, that connection can remain owned by the context.
ActiveRecord::Base.clear_active_connections!: Rarely needed in application code, but useful in test helpers or custom thread-management code that needs to release owned connections eagerly.
Setting
Where configured
Default
Effect
pool
config/database.yml
RAILS_MAX_THREADS, or 5 in the generated configuration
Maximum connections in one pool
checkout_timeout
config/database.yml
5 seconds
Maximum wait for an available connection
idle_timeout
config/database.yml
300 seconds
Idle period after which a connection may be reaped
Common Pitfalls
Pool exhaustion: A timeout means every slot is occupied. Check pool sizing, long transactions, slow queries, and connection leaks rather than only increasing the timeout.
Custom threads: Calling connection inside a manually created thread without releasing it can retain a pool slot for that thread’s lifetime.
Slow work inside database blocks: HTTP calls, file processing, or sleeps performed while holding a connection unnecessarily reduce pool availability.
Assuming the pool is global: Every process and configured database role has an independent pool, so sizing and monitoring must be performed per process and per pool.
46
In Rails, what is meant by the fat-model, skinny-controller design, and why is it recommended?
KnownRevise
The fat-model, skinny-controller approach means putting business rules, validations, and data logic in Active Record models while keeping controllers thin: a controller processes the HTTP request, invokes a meaningful domain operation, and renders or redirects.
Read full answerHide full answer
Applies toRuby on Rails 8 (the pattern itself is version-independent; the Solid Queue default statements refer to newly generated Rails 8 applications)
What Counts as Fat and Skinny
Model responsibilities: validations, scopes, associations, enums, computed or derived values, state transitions, business rules, and wrapping changes owned by the model in a transaction.
Controller responsibilities: authentication orchestration, invoking authorization policies, strong parameters, invoking the domain operation, choosing a response (render, redirect_to, status codes, flash), and setting instance variables for the view.
“Skinny” does not mean empty: an action can still branch for success versus validation failure and for HTML versus Turbo Stream responses. It means the controller holds no business decisions.
Why the Separation Matters
Reusability: the same rule must often run from an HTML request, a JSON API, a Turbo Stream response, an Active Job worker, a mailer, the console, or a rake task. Logic in a model is available everywhere; logic in a controller is not.
Testability: domain model objects can be unit-tested without an HTTP request. Controller tests and system tests are slower and more awkward.
Single source of truth: a rule such as “an article cannot be published without a body” lives in one place, so it cannot drift between endpoints.
Change safety: adding a second entry point, such as a mobile API or admin panel, does not duplicate logic or introduce inconsistent behaviour.
How a Request Flows in Rails 8
The router dispatches to a controller action.
A before_action runs authentication and invokes the relevant authorization policy. The generator bin/rails generate authentication, introduced in Rails 8, creates a User model, a Session model, an Authentication concern, and sessions and passwords controllers; it illustrates controller orchestration backed by model logic.
The action builds permitted attributes with strong parameters, for example params.require(:article).permit(:title, :body).
The action calls a model method or domain service. The model enforces validations and callbacks, defines scopes, and can wrap changes it owns in a transaction.
The action responds with redirect_to on success or render :new, status: :unprocessable_entity on validation failure. The status matters for Turbo form submissions in Rails 7 and 8.
Slow or external work is pushed to Active Job rather than blocking the request. Solid Queue is the default Active Job backend in newly generated Rails 8 applications.
Concern
Controller
Model / domain layer
HTTP status, redirects, flash
Yes
No
Strong parameters
Yes
No
Authorization policy rules
No
No; use a policy layer
Validations and scopes
No
Yes
Business rules and state changes
No
Yes
Transaction boundaries
Rarely
Usually
Authentication orchestration
Yes
No
When “Fat” Goes Wrong
God objects: a model that grows past a few hundred lines becomes hard to test and reason about. Split it with concerns, query objects, form objects built on ActiveModel::Attributes, or service objects for multi-model workflows.
Side effects in callbacks: sending email or calling an external API inside a before_save or after_create makes saving slow and surprising. Enqueue a job instead.
N+1 queries: lazy loading from a fat model method or a view triggers a query per row. Load associations up front with includes, preload, or eager_load.
Bypassing validations: update_attribute skips validations but runs callbacks; update_column skips both validations and callbacks. Prefer update or update! unless deliberately bypassing checks.
Logic leaking upward or downward: business if/else chains in controllers, or SQL built in views, both defeat the pattern.
Best Practices
Keep each action focused on orchestration rather than enforcing an arbitrary one-query limit; a domain operation may legitimately perform several queries.
Name model methods after the business operation (publish!, archive!, recalculate_total!) rather than the column being changed.
Use find_by with a nil check, or find with a rescue for ActiveRecord::RecordNotFound, consistently.
Never interpolate user input into SQL; use hash conditions or bound parameters.
Move repeated validation or scoping into concerns, and repeated multi-step flows into service or form objects once a model starts to bloat.
Test models directly for rules and edge cases, and reserve controller and system tests for HTTP behaviour.
47
In Rails 8, what purpose do service objects serve, and which kinds of business operations justify introducing one?
KnownRevise
A service object is a plain Ruby class that encapsulates a business operation and coordinates the models, services, or external systems required to perform it. It is useful when an action is too complex for a controller or a single model method, but it should complement—not replace—Active Record validations and well-designed domain models.
Read full answerHide full answer
Applies toRuby on Rails 8
Definition and Purpose
Rails does not provide a special service-object framework or mandate a particular implementation. The common convention is to place these classes under app/services, where Rails’ normal Zeitwerk autoloading applies.
A service object usually represents an intentional business action, such as:
PlaceOrder
CancelSubscription
PublishArticle
ImportCustomers
Its public API should express the operation clearly—for example, PlaceOrder.call(...) or an equivalent instance method. The implementation can coordinate multiple records, apply business rules, invoke external APIs, and control a database transaction.
Service objects should generally avoid HTTP-specific responsibilities such as reading params, rendering responses, or setting redirects.
When to Use One
Use a service object when the operation involves several of these characteristics:
Multiple models or aggregates: For example, creating an order, reserving inventory, and recording a payment.
Significant branching: The behavior depends on domain rules that would make a controller or model method unwieldy.
External integrations: An operation might charge a payment provider, send a notification, and update local records.
Explicit transaction boundaries: Several writes must succeed together or leave the database unchanged.
Reusable business behavior: The same operation may be invoked through a controller, background job, command, or another service.
Clear separation of orchestration: A controller can remain focused on transport concerns while the service handles the use case.
A checkout flow is a typical example. It might validate a cart, check stock, create an order, process payment, and reserve inventory. Moving that orchestration into a service gives it a meaningful boundary and makes it easier to test and reuse.
When Not to Use One
Do not introduce a service object merely because a controller is long. First consider whether the behavior has a simpler, more natural home.
Situation
Better default
Standard create, update, or delete
Controller plus model validations
An invariant involving one record
Model method or validation
Basic persistence behavior
Active Record callback
Domain calculation with no side effects
Model method, value object, or plain Ruby class
Complex multi-record business process
Service object
Model validations should remain responsible for enforcing record-level invariants. Callbacks can handle tightly coupled model lifecycle behavior, but they should not become a general-purpose container for arbitrary business workflows. Overusing callbacks often creates hidden control flow and makes execution order difficult to understand.
Design Responsibilities
A well-designed service object should:
Accept explicit inputs: Pass validated data or domain objects rather than accessing controller state.
Perform one business operation: Avoid unrelated “god objects” that handle many unrelated workflows.
Coordinate dependencies: Inject collaborators when practical so workflows can be tested without live external services.
Manage transactions correctly: Place database writes that must commit together inside one transaction. Use operations that raise on failure, such as save!, when an unsuccessful write should abort the transaction.
Expose meaningful outcomes: Return a result object or domain value instead of silently ignoring errors.
Keep authorization outside: Authorize the caller before invoking the operation rather than scattering authorization checks through its internal steps.
The service should not bypass validations to force persistence. Validated and persisted models remain important safeguards even when a service coordinates the broader workflow.
Common Pitfalls and Best Practices
Avoid anemic models: Moving all model behavior into services can leave Active Record classes as passive database wrappers.
Keep transactions narrow: Do not hold a database transaction open while waiting for a slow external API. Prefer a state machine, outbox pattern, or background job for reliable asynchronous processing.
Make retries safe: External side effects may repeat after a timeout. Use idempotency keys where the provider supports them.
Do not swallow exceptions: Translate technical failures into meaningful domain errors only when the abstraction is genuinely useful.
Prefer clear interfaces: A concise operation such as CancelSubscription.call(user:, reason:) communicates more than a generic execute(params) method.
Test behavior rather than implementation: Verify business outcomes, transaction behavior, and integration calls without coupling every test to internal method order.
Service objects are most valuable as focused application-layer boundaries for meaningful business operations, not as a mandatory layer for every controller action.
48
How would you apply the decorator pattern within a Rails application?
KnownRevise
The decorator pattern in Rails wraps an existing object with additional behavior while delegating its original interface. Rails 8 does not provide a decorator generator or required base class, so a plain Ruby wrapper is often sufficient and keeps view-specific behavior separate from Active Record models.
Read full answerHide full answer
Applies toRuby on Rails 8
Core Idea
A decorator uses composition rather than inheritance:
The Active Record model remains responsible for persistence, validations and domain behavior.
The decorator exposes the model’s existing methods, usually through Active Support’s delegate.
New formatting or presentation methods are added by the decorator.
The original model is not modified or subclassed.
For example, a User model might expose first_name, last_name and email, while a UserDecorator adds display_name and account_status for use in views.
Choosing the Right Abstraction
Requirement
Recommended approach
Database persistence and validation
Active Record model
View-specific formatting
Decorator
Behavior needed throughout the domain
Model method or model concern
Multi-step business workflow
Service object
Policy or authorization decision
Authorization framework such as Pundit
A decorator is not a substitute for a service object. Decorators should generally be lightweight and avoid coordinating writes, transactions or complex business workflows.
Code Example
Assume the User model contains first_name, last_name, email and an active boolean.
# app/decorators/user_decorator.rb
class UserDecorator
delegate :id, :first_name, :last_name, :email, to: :user
def initialize(user)
@user = user
end
def display_name
full_name = [user.first_name, user.last_name].compact.join(" ").strip
full_name.presence || user.email
end
def account_status
user.active? ? "Active" : "Inactive"
end
private
attr_reader :user
end
Create the decorator in the controller and pass it to the view:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find_by!(id: params[:id])
@user_decorator = UserDecorator.new(@user)
end
end
Use the delegated and decorated methods in the template:
delegate preserves the familiar model interface without requiring the decorator to inherit from User. The ERB output is automatically escaped by default; avoid raw or html_safe unless the value has been deliberately sanitized.
Working with Collections
Decorating records does not itself cause database queries. However, accessing associations through delegated methods while rendering a collection can produce N+1 queries. Preload or eager-load the required associations before creating the decorators:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.includes(:team).order(:last_name)
@user_decorators = @users.map { |user| UserDecorator.new(user) }
end
end
Use includes or preload for separate association queries, and eager_load when conditions or joins require the associated records in the same query.
Best Practices and Common Pitfalls
Keep persistence in the model: Do not move validations, callbacks or database operations into a presentation decorator.
Use strong parameters for updates: Permit only accepted attributes in mutation endpoints, and place multi-record writes in a service object.
Keep HTML out of decorators: Return plain values and let the view decide how to render them.
Preload associations: Decoration does not automatically solve N+1 query problems.
Avoid unnecessary delegation: Explicitly delegate the interface the view needs instead of exposing the entire model unintentionally.
Test both layers: Unit-test the decorator with a model instance or test double, and use request or system tests to verify its integration.
Check gem compatibility: If using a third-party decorator library, select a release that explicitly supports Rails 8 rather than relying on older Rails compatibility declarations.
49
Which recurring design and implementation mistakes are common in Ruby on Rails applications, and how can they be corrected?
KnownRevise
Common Rails anti-patterns often place too much responsibility in controllers or models, cause inefficient database access, or weaken application security. The central principle is to keep clear separation of concerns while using Rails’ conventions for validation, persistence, and input handling.
Read full answerHide full answer
Applies toRuby on Rails 8
Common Anti-Patterns
Fat controllers: A controller that validates input, performs complex calculations, coordinates several records, sends emails, and builds external API responses becomes difficult to test and maintain. Keep controllers focused on receiving requests, invoking application behavior, and producing responses.
“Fat model” dumping grounds: Moving arbitrary business logic into an Active Record model does not automatically improve design. Models should primarily represent domain data and persistence behavior; larger workflows may belong in dedicated service objects, form objects, or other well-named components.
Hidden work in callbacks: Side effects such as sending email, publishing events, or calling third-party services inside before_save or after_save make execution order difficult to understand. Prefer explicit methods, and use after_commit when an external side effect must happen only after the database transaction succeeds.
Overengineering: Creating a service object, module, or abstraction for every method can make a simple application harder to navigate. Extract components when responsibilities are substantial, reused, or independently testable rather than for trivial delegation.
Misusing associations: Defining associations only for convenience can produce accidental joins, incorrect query behavior, or large object graphs. Use associations when they reflect the domain relationship and configure their behavior deliberately.
Database Access and Performance
N+1 queries: Iterating over records and then accessing their associations commonly executes one query per record. For example, rendering post.comments for many posts can trigger a query for every post.
Loading unnecessary data: includes or eager_load can also be excessive when only a small part of an association is needed. Select specific columns where appropriate, but do not exclude columns required by the application or by Active Record behavior.
Relying on lazy loading in views: Queries triggered while rendering templates are difficult to monitor and optimize. Load the required associations before rendering.
Using exceptions for normal control flow: find_by! is appropriate when absence should stop processing, but repeatedly rescuing ActiveRecord::RecordNotFound for ordinary conditional logic is unnecessarily expensive and obscures intent. Use find_by when absence is an expected result.
Ignoring concurrency: Read-modify-write operations without suitable locking or optimistic concurrency controls can lose updates. Use transactions, row locking, or a locking column when concurrent modification is possible.
Security and Validation
Mass assignment without strong parameters: Never pass unrestricted request parameters directly to create or update. Explicitly permit each permitted attribute with permit.
Interpolating input into SQL: Strings containing user input must never be inserted into SQL fragments. Use hash conditions or bound parameters, and keep dynamic SQL identifiers restricted to a trusted allowlist.
Treating valid? as validation: valid? reports whether a record is valid but does not save it. Use save, save!, update, or update! according to whether failures should be returned or raised.
Skipping model-level validation: Controller-only checks can be bypassed by console jobs, imports, or future endpoints. Use Active Record validations for domain invariants and strong parameters for controlling permitted attributes.
Rendering untrusted content unsafely: User-provided HTML or JavaScript must be escaped or sanitized with an appropriate, maintained sanitization library. Do not mark arbitrary user content as safe.
Recommended Rails Approaches
Anti-pattern
Better approach
Business workflow inside a controller
Call a well-named model method, service object, or form object
External side effects in persistence callbacks
Invoke them explicitly or use after_commit
N+1 association loading
Use includes, preload, or eager_load strategically
Unrestricted parameter assignment
Use strong parameters and permit
String-built SQL
Use hash conditions or bound query parameters
One service object for every operation
Keep simple behavior local and extract only meaningful units
Missing update concurrency protection
Use transactions, locks, or optimistic locking
Practical Guidance
Query logs and Active Record query counts help identify N+1 behavior; enable them in development and test environments rather than production request paths.
Keep authorization close to the resource being accessed, and verify that the current user may act on the specific record rather than merely on its type.
Use transactions around multi-record consistency changes, but keep external network calls outside database transactions where possible.
Prefer explicit orchestration for business workflows. This makes execution order visible and lets tests verify each step without relying on framework callbacks.
Use preload for separate association queries, eager_load when joined loading and joined conditions are needed, and includes when the appropriate strategy can be selected by Active Record.
Use strict_loading where appropriate to turn accidental association loading into a detectable error in development and test environments.
50
Explain what a presenter contributes to the architecture of a Rails application.
KnownRevise
A presenter (also called a view model or view object) is a plain Ruby class that owns the presentation logic for a single domain object or a single screen, sitting between the controller and the view so that models keep their persistence rules and templates stay free of formatting and conditionals.
Read full answerHide full answer
Applies toRails 8 (presenters are an application-level pattern, not a framework component, so the same approach works in earlier Rails versions).
Where a Presenter Sits in the Stack
The request flow becomes: model (persistence and business rules) → controller (params, authorization, queries) → presenter (formatting and display decisions) → view (ERB/Turbo templates).
Not part of Rails: no generator, base class or configuration exists for presenters. You create app/presenters/post_presenter.rb, and Zeitwerk autoloads it like any other app/* directory.
Read-only layer: a presenter wraps one object (or a collection) and exposes display-friendly methods; it never writes to the database.
Instantiated per request: the controller builds it with the record and the view context, then hands it to the template.
What a Presenter Is Responsible For
Formatting: turning published_at into a localized string, money into number_to_currency, enums into human labels.
Display decisions: “Published” vs “Draft” badges, empty-state text, CSS class names for a state.
Aggregation for one screen: combining several records into the shape a dashboard or profile page needs.
Trimming templates: moving multi-branch if/case logic out of ERB so the template reads as markup.
Making logic testable: plain Ruby objects can be unit tested without rendering a view.
Presenter vs. Helper, Decorator, Serializer
Concern
Use when
Example
Presenter
Display logic for one object or screen
PostPresenter#status
View helper
Small, generic formatting reused everywhere
number_to_currency
Decorator
Per-object view logic with delegation, via third-party gems such as draper
post.decorate
Serializer
JSON for an API, not HTML
PostSerializer
Service object
An action that changes state
PublishPost.call(post)
Helpers are global and mixin-based; presenters are scoped, injected and easier to test. Service objects change data, presenters only describe it.
Best Practices and Pitfalls
Keep it read-only: no save, update or side effects; that belongs in a model method or a service object.
Don’t duplicate model rules: validations and invariants stay on the model; the presenter only decides how they look.
Never query inside a presenter: load associations with includes, preload or eager_load in the controller, or each render will trigger extra queries.
Inject view_context instead of reaching for global helpers, so the presenter can be tested in isolation.
Avoid god objects: if a presenter grows past a screen’s needs, split it per screen or extract partials (or ViewComponent, which is a separate gem, not Rails core).
Know when to skip it: for one template with one formatting rule, a private helper method or a partial is simpler. Presenters pay off when an object is rendered in several places or the logic needs real tests.
With Hotwire: the presenter supplies formatted data and the Turbo Frame or Turbo Stream partial only renders it, which keeps stream updates small and consistent.
51
What approaches do you take to split a sprawling Rails codebase into separate, well-bounded modules?
KnownRevise
Modularizing a large Rails application means carving it into bounded units — namespaced components, mountable Rails engines, Packwerk-enforced packs, or extracted services — each exposing a small public interface and hiding its internals. The goal of modularization is to stop unrelated domains from depending on each other’s details, so any one slice can be changed, tested and reasoned about on its own.
Read full answerHide full answer
Applies toRails 8 with Ruby 3.x; version notes are called out where behaviour changed.
First step; splitting folders, controllers and jobs by domain
Mountable engine with isolate_namespace
Medium — own routes, views, migrations
Domain slices, admin panels, reusable features
Packwerk-enforced packs
Medium — static dependency checks
Enforcing boundaries in a monolith without engines
Separate application/service
High — own deploy and database
Different scaling, security or release cadence
How Engines Work
Engine: a subclass of Rails::Engine packaged as a gem that brings its own app/, config/routes.rb, migrations and initializers into the host app.
isolate_namespace: places the engine’s controllers, models and helpers under its own constant, so Billing::Invoice cannot collide with the host’s Invoice.
Route mounting: the engine draws routes in Billing::Engine.routes.draw and the host mounts the engine at a prefix, keeping URL space separated.
Railtie: the lighter-weight hook when you only need initializers or configuration, not a full app structure.
Migrations: the engine owns them; the host installs copies with the per-engine task (for example bin/rails billing:install:migrations) rather than editing the engine’s schema.
Enforcing Boundaries in Rails 8
Zeitwerk autoloading: config.autoload_lib(ignore: %w[assets tasks]) (introduced in Rails 7.1 and unchanged in Rails 8) lets plain lib/ code be autoloaded, so components can live outside app/ without manual require.
Static checks: the packwerk gem flags constants that cross pack boundaries; run it in CI so violations fail the build.
Gems: extract genuinely reusable code into a versioned gem and depend on it explicitly.
Infrastructure: Rails 8’s generated production setup uses Solid Cache, Solid Queue and Solid Cable, each backed by its own database. Solid Queue can run as a separate bin/jobs process, or inside Puma for a single-server deployment via the Puma plugin, so background work can be decoupled from the web tier when needed.
Best Practices and Pitfalls
Start with namespaces, not engines: split folders and controllers first; promote to an engine only when the boundary has proven stable.
Keep the dependency graph acyclic: high-level packs may depend on lower-level ones, never the reverse; circular references are the main reason modularization stalls.
Talk through public APIs: expose a small service object or model method instead of letting one component query another’s tables or call its private scopes.
Beware N+1 queries across boundaries: when a component serializes records it fetched itself, use includes/preload, and mark associations strict_loading to surface lazy loads early.
Avoid callback-driven coupling: an after_create in one domain that silently triggers another’s work is hidden coupling; publish an explicit event or enqueue a job instead.
Keep authorization and strong parameters local: each component permits only its own attributes — never fall back to permit!.
Do not distribute prematurely: a network hop adds failure modes, latency and versioning overhead that a well-isolated engine inside one deploy avoids.
52
How does Rails implement Single Table Inheritance and polymorphic associations, and when should each pattern be used?
KnownRevise
Rails supports Single Table Inheritance (STI) for storing related model subclasses in one table and polymorphic associations for associating a model with several possible target types. STI models a shared hierarchy, while polymorphism models a “belongs to one of many” relationship without requiring a common superclass.
Read full answerHide full answer
Applies toRuby on Rails 8
Single Table Inheritance
With STI, Rails stores every record in a single database table and adds an inheritance discriminator, conventionally named type.
User is the base class and represents the shared table.
AdminUser and CustomerUser inherit from User but add type-specific behavior.
The type column contains the STI class name, such as AdminUser.
User.all returns records of every STI type, while AdminUser.all returns only AdminUser records.
Columns, validations, associations, scopes, and callbacks defined on User are inherited.
Subclasses do not automatically have separate tables; that requires a different persistence design.
The inheritance column can be customized with self.inheritance_column, although type remains the conventional choice. By default, Rails stores the full STI class name in the discriminator.
Polymorphic Associations
A polymorphic belongs_to association allows a model such as Comment to belong to a Post, Video, or another supported model.
Rails stores two values:
commentable_id: the target record’s primary key.
commentable_type: the target model’s base-class identifier.
The inverse side declares the association name with as:. Only the belongs_to side needs polymorphic: true; Rails uses the association name to derive the conventional foreign-key and foreign-type columns. Custom names can be supplied with foreign_key and foreign_type.
Key Differences
Concern
Single Table Inheritance
Polymorphic Association
Purpose
Represent specialized subclasses of one model
Associate a model with multiple possible types
Database structure
One table with a type discriminator
Foreign-key and foreign-type columns
Model relationship
Inheritance-based
Association-based
Typical example
User, AdminUser, CustomerUser
Comment belonging to a post or video
Main risk
Invalid or renamed type values
Weak database-level referential integrity
When to Use Each Pattern
Use STI when subclasses share most attributes and behavior and can reasonably be stored in one table. Typical examples include different user roles or specialized kinds of the same domain object. Use separate tables when their fields, validation rules, query patterns, or persistence lifecycles differ substantially.
Use polymorphic associations when a relationship genuinely has varied target types and those types do not need a meaningful common superclass. For example, comments can belong to posts, videos, or other attachable records. If every target shares important fields and behavior, introducing a common superclass may produce a clearer domain model.
Best Practices and Common Pitfalls
Treat STI type values as persistent data. Renaming a subclass without migrating the stored discriminator can make existing records difficult to query or load.
Prevent N+1 queries with includes(:commentable) or preload(:commentable). Preloading polymorphic associations may issue one query for each concrete target type.
Never accept an unrestricted polymorphic type from user input. Whitelist allowed types, validate the association, and perform authorization checks.
A conventional database foreign key cannot reference whichever of several tables the polymorphic type selects. Enforce these relationships with validations, application authorization, and appropriate cleanup or consistency processes.
For STI, consider validations and scopes carefully because subclasses inherit their parent’s behavior and may require distinct constraints or rules.
53
In Rails, what exactly is a concern, and what is the right way to use one?
KnownRevise
A concern in Rails is a Ruby module that bundles one coherent piece of behaviour — validations, callbacks, scopes, associations or controller filters — so several models or controllers can share it by including it. The convention is to build it on ActiveSupport::Concern, which adds an included/class_methods DSL and handles module dependencies.
Read full answerHide full answer
Applies toRails 8.0 (the `ActiveSupport::Concern` DSL described here exists in Rails 5 and later; the guard against multiple `included` blocks was added in a later release)
What a Concern Is
A plain module: nothing stops you using a normal Ruby module with self.included(base) and base.extend, but ActiveSupport::Concern gives you a cleaner, safer version of that pattern.
Autoloaded location: Rails already loads every app/* directory plus app/*/concerns, so app/models/concerns and app/controllers/concerns are on the autoload and eager-load paths and a file there needs no require. Under Zeitwerk the file name must match the constant: app/models/concerns/trackable.rb defines Trackable.
One included block, safe re-inclusion: including the same concern twice is harmless (the second include is skipped), but a concern must define a single included block — declaring a second, different one raises ActiveSupport::Concern::MultipleIncludedBlocks. This is why concerns are safer than hand-written included hooks, which re-run on every include.
How ActiveSupport::Concern Works
extend ActiveSupport::Concern marks the module as a concern.
Instance methods written in the module body become instance methods of the including class.
included do ... end is evaluated with class_eval in the including class, so scope, validates, has_many and before_action register on that class.
class_methods do ... end defines methods that are extended onto the including class.
The concern records its dependencies, so nested includes resolve in the right order and are not duplicated.
Where Concerns Are Used
Model concerns: shared behaviour such as soft deletes (Discardable), slugs (Sluggable), auditing stamps, or a shared published scope.
Controller concerns: cross-cutting request handling such as locale selection, pagination, or authentication. The authentication generator introduced in Rails 8 (bin/rails generate authentication) generates an Authentication concern in app/controllers/concerns/authentication.rb alongside a Session model, a User model and Current attributes.
Not a dumping ground: if two classes share code but not a concept, prefer inheritance, plain objects or delegation instead of a concern.
Code Example
# app/models/concerns/publishable.rb
# Assumes the host model has a boolean `published` column and a datetime `published_at`.
module Publishable
extend ActiveSupport::Concern
included do
# Evaluated in the including model: scope, validation and callback all land there.
scope :published, -> { where(published: true) }
validates :published_at, presence: true, if: :published?
before_validation :stamp_published_at # before_validation, so the stamp runs before the check
end
class_methods do
# Becomes a class method on the including model.
def published_this_week
published.where(published_at: Time.current.all_week)
end
end
private
def stamp_published_at
self.published_at = Time.current if published? && published_at.nil?
end
end
class Article < ApplicationRecord
include Publishable # gains the scope, the validation, the callback and the class method
end
# app/controllers/concerns/locale_switchable.rb
module LocaleSwitchable
extend ActiveSupport::Concern
included do
before_action :set_locale # filters can be declared the same way as model macros
end
private
def set_locale
I18n.locale = preferred_locale
end
def preferred_locale
requested = request.headers["Accept-Language"].to_s.split(",").filter_map do |part|
part.split(";").first.to_s.strip.split("-").first.presence
end
(requested & I18n.available_locales.map(&:to_s)).first&.to_sym || I18n.default_locale
end
end
Best Practices and Common Pitfalls
One responsibility per concern: a concern named Auditable that also sends emails and paginates is a junk drawer; split it.
Watch for silent overrides: a method or scope in a concern is shadowed without warning if the including class defines the same name, so keep names specific.
Callbacks affect every includer: a before_save in a widely included concern changes behaviour across the whole app; document it and keep the logic small.
Avoid hidden state: concerns work best when they operate on columns and methods the host class already has; putting unrelated state in them makes models harder to reason about.
Keep concerns shallow: deep chains of concerns including other concerns become hard to debug; a single level of composition is usually enough.
Test the concept, not the module only: cover the behaviour through a real including model so collisions and callback ordering show up.
54
In Rails, what is the approach for building real-time functionality, for example with Action Cable?
KnownRevise
Real-time features in Rails 8 use Action Cable to maintain a long-lived browser-to-server connection, normally over WebSocket with HTTP long-polling as a fallback. For browser-driven HTML updates, combine Action Cable with Turbo Streams; newly generated Rails 8 applications use Solid Cable as the production adapter, so Redis is not required by default.
Connection: ApplicationCable::Connection establishes and authenticates each Action Cable connection. Use identified_by :current_user to make the authenticated user available to channels, and reject unauthorized connections.
Channels: ApplicationCable::Channel subclasses contain server-side subscription logic. One Action Cable connection can multiplex several channel subscriptions.
Streams: Streams are named broadcast destinations. A channel authorizes the requester and then registers the appropriate stream, commonly with stream_for.
Turbo Streams: The turbo_stream_from helper creates a signed Turbo Stream subscription and renders a <turbo-cable-stream-source> element. Turbo then applies delivered HTML fragments without custom JavaScript.
Adapters: config/cable.yml selects the pub/sub backend. Development uses the in-process async adapter, while Rails 8-generated production applications use solid_cable. Rails 7-generated applications generally used the Redis adapter.
The Recommended Path: Turbo Streams
Use Turbo Stream broadcasting for feeds, notifications, comments, and other live HTML fragments. Broadcast with methods such as Turbo::StreamsChannel.broadcast_append_to.
A model’s broadcasts_to macro can automatically broadcast after create, update, and destroy to the model’s stream. The corresponding broadcast_*_later_to methods render through Active Job.
Broadcasts render the same partials used by normal requests, so normal HTML escaping applies. Controller authorization does not automatically apply; data access and authorization must be enforced separately.
Signed stream identifiers prevent clients from forging stream names, but signing is not access control. Do not place private data on a broadly accessible stream. If subscription-time authorization is required, use a custom channel or a separately authorized application flow.
Turbo Streams are usually the simplest choice for HTML updates, while custom channels remain appropriate for presence, typing indicators, and application-specific JSON protocols.
Custom Channels vs Turbo Streams
Requirement
Best fit
Live HTML fragments, feeds, notifications, and CRUD updates
Turbo Streams broadcasting
Presence, typing indicators, and custom JSON protocols
Custom Action Cable channel
Non-Rails or mobile clients
Custom channel or a separate real-time service
For a custom channel, authorize inside subscribed, derive the stream from an authorized record, and register it with stream_for. Use RoomChannel.broadcast_to to publish a payload to that record’s stream.
Scaling, Security, and Performance
Re-check the current user’s access to the underlying record whenever a channel subscribes; never trust client-supplied identifiers alone.
Configure config.action_cable.allowed_request_origins appropriately and keep Action Cable’s request forgery protection enabled for browser clients.
Solid Cable stores messages in the solid_cable_messages table and polls for them. Its polling_interval and message_retention settings are configured in config/cable.yml; Rails 8 applications load the cable schema with bin/rails db:prepare.
Capacity-plan Solid Cable’s database writes and polling. The Redis adapter remains supported for deployments that prefer external pub/sub.
Keep rendered partials small. Preload associations in the execution path that performs delayed rendering; preloading only in the original controller request does not carry over to a later Active Job.
Best Practices and Common Pitfalls
Prefer Turbo Streams for HTML updates and custom channels for non-HTML protocols.
Do not publish records from an uncommitted transaction. Use commit-aware callbacks, such as after_commit, or the commit-aware broadcasts_to macro.
Use broadcast_*_later_to when rendering could make a request slow, while accounting for the resulting job latency.
Never derive stream names by interpolating raw user input; derive them from authorized records or use signed helpers.
Use the test adapter in config/cable.yml for automated tests so broadcasts can be captured and asserted directly.
55
How can a Rails 8 application generate and return PDF documents?
KnownRevise
Rails does not include a built-in PDF renderer; PDF generation is normally implemented with a dedicated Ruby library such as Prawn or by rendering HTML with a headless browser. For documents such as invoices and reports, Prawn is often simpler because it produces PDFs directly without an external browser dependency.
Read full answerHide full answer
Applies toRails 8 with Prawn 2.5
Main Approaches
Prawn: A pure-Ruby PDF-generation library that gives precise control over text, tables, images, and page layouts.
Headless Chromium: Converts HTML and CSS into PDF, making it suitable when the PDF should closely resemble a web page. This requires managing a browser process in the deployment environment.
External PDF service: Sends document data to a hosted rendering API. This reduces infrastructure work but introduces network, cost, privacy, and availability considerations.
How the Prawn Approach Works
Prawn builds a Prawn::Document and then serializes it into PDF bytes. Those bytes can be returned directly from a Rails controller using send_data.
A typical application separates responsibilities as follows:
The controller authorizes the request and retrieves the record.
A service object builds the PDF.
The controller sets the response type and disposition.
The browser either displays the PDF inline or downloads it as an attachment.
This keeps rendering logic outside the controller and makes it easier to test or reuse.
Content, Layout, and Data
Prawn is a low-level document API rather than an HTML renderer:
Use text, table, and image to add content.
Use font, font_size, and text options for typography.
Use bounding_box to control layout and start_new_page to control pagination.
Add page numbers and repeated headers manually as documents grow.
Register a TrueType font when non-Latin characters are required; Prawn’s default fonts have limited Unicode support.
Prawn does not interpret HTML, CSS, or JavaScript. If HTML-to-PDF rendering is required, use a headless-browser approach and test the result with print-specific CSS.
Runtime and Deployment
The Prawn approach has no browser runtime requirement, which simplifies deployment and improves predictability. However, Prawn::Document#render creates the complete document in memory. For large files, generate the PDF in a background job and store the result in object storage rather than holding it in a web request.
With headless Chromium, account for:
Browser and shared-library installation.
Memory consumption from concurrent rendering processes.
Waiting for JavaScript and web fonts before printing.
Isolation of the renderer from untrusted HTML.
Differences between screen CSS and @media print output.
Code Example
Add Prawn to the application:
# Gemfile
gem "prawn", "~> 2.5"
Create a service that renders an invoice:
# app/services/invoice_pdf.rb
class InvoicePdf
def initialize(invoice)
@invoice = invoice
end
def render
Prawn::Document.new do |pdf|
pdf.text "Invoice ##{@invoice.id}"
pdf.move_down 12
rows = [
["Description", "Amount"],
[@invoice.description, format_amount(@invoice.total)]
]
pdf.table(rows) do
# Highlight the table header.
style row(0), background_color: "EEEEEE"
# Right-align monetary values.
style column(1), align: :right
end
end.render
end
private
def format_amount(amount)
format("%.2f", amount)
end
end
Return it from a scoped controller action:
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def pdf
# Scoping through current_user prevents access to another user's invoice.
invoice = current_user.invoices.find(params[:id])
send_data InvoicePdf.new(invoice).render,
filename: "invoice-#{invoice.id}.pdf",
type: "application/pdf",
disposition: "inline"
end
end
The corresponding route can be declared as:
# config/routes.rb
Rails.application.routes.draw do
resources :invoices, only: [:show] do
get :pdf, on: :member
end
end
Best Practices and Common Pitfalls
Authorize every document: Retrieve records through the current user, account, or another authorized scope.
Avoid untrusted HTML: If using a browser renderer, sanitize user-supplied markup and prevent access to internal network resources.
Use stable filenames: Prefer server-generated identifiers rather than inserting raw user input into response filenames.
Test the rendered output: Automated tests can verify PDF bytes and metadata, while visual regression tests are useful for layout-sensitive documents.
Handle large documents asynchronously: Move generation to a background job and return a signed download URL when appropriate.
Pin production dependencies: Commit a lockfile so Rails, Prawn, fonts, and browser tooling remain reproducible across deployments.
56
What role does Rack play in Ruby web development, and how does a Rails application use it to process HTTP requests?
KnownRevise
Rack is the low-level interface between a Ruby web server and a web application. Rails implements this interface, so Rack servers can forward HTTP requests to Rails and receive responses through a standardized protocol.
Read full answerHide full answer
Applies toRails 8 with Rack 3.x
What Rack Provides
Rack defines how Ruby applications receive requests and return responses. It is not itself a complete web framework or application server; rather, it provides a common contract that servers such as Puma and frameworks such as Rails can implement.
Component
Responsibility
Rack specification
Defines the request, response, and middleware interfaces
Rack application
Receives a request environment and returns status, headers, and a response body
Rack middleware
Processes or transforms requests and responses around an application
Rack-compatible server
Accepts network connections and invokes the Rack application
This separation allows the same Rails application to run under different Rack-compatible servers without rewriting its request-handling code.
The Rack Request and Response Contract
A Rack application is callable and receives an env hash containing information about the HTTP request. Important entries include:
REQUEST_METHOD: The HTTP method, such as GET or POST.
PATH_INFO: The requested path.
QUERY_STRING: The raw query string.
rack.input: An input stream containing the request body.
rack.errors: The application’s error-output stream.
HTTP headers such as HTTP_HOST and HTTP_COOKIE.
The application returns a three-element array: a numeric HTTP status code, a headers hash, and a Rack-compatible response-body object. Under Rack 3, response header names must use lowercase keys, and response bodies must yield strings. Rails handles these Rack requirements internally, so application developers normally work with Rails abstractions instead.
How Rails Uses Rack
Rails.application is a Rack application. Its request-processing path generally works as follows:
A Rack-compatible server such as Puma receives the HTTP request.
The server invokes the Rails Rack endpoint with a Rack environment.
Rails middleware processes concerns such as host authorization, cookies, sessions, static files, security headers, and request identifiers.
ActionDispatch::Request wraps the Rack environment and provides Rails-friendly access to parameters, headers, cookies, and the request body.
The Rails router identifies the controller action.
The controller and view generate the response.
Rails converts that response into Rack’s status, headers, and body, which the server sends to the client.
Rails features such as params, cookies, sessions, and CSRF protection are built on top of Rack but are not part of the Rack specification itself. In particular, under Rack 3, application code should use the Rails session object rather than assuming that session data is exposed through a specific env key.
Middleware and Server Boundaries
Rack middleware wraps the next application in the stack. A request travels through each middleware before reaching the Rails application, and the response travels back through the same middlewares. Each middleware can inspect or modify the request environment before calling the next application. It can also inspect or replace the response after that application returns.
Rails keeps most application behavior in controllers, models, jobs, and services. Middleware is best reserved for cross-cutting request concerns. Puma handles the server side by accepting network traffic and invoking the Rack endpoint; Rails generally remains responsible for application-level behavior.
A Rails application’s config.ru exposes the Rails endpoint to a Rack server: it boots the application and runs Rails.application, which is the entry point used by rackup-style bootstrapping and by Rack-compatible servers. In development, bin/rails server normally starts Puma, which loads this Rack endpoint. In production, Puma is commonly run as an application server behind a reverse proxy or load balancer that terminates HTTPS.
Best Practices and Common Pitfalls
Keep business logic out of middleware: Put domain behavior in controllers, services, or model-layer objects. Middleware should handle reusable HTTP-level concerns.
Use Rails request abstractions: Prefer params, request, cookies, and session instead of reading raw env values unnecessarily.
Preserve the Rack contract: Middleware must return a valid status, headers, and body and should preserve streaming behavior where applicable.
Use Rack 3 header conventions: Custom middleware should insert lowercase response-header names.
Protect credentials and sessions: Supply secret_key_base through Rails credentials or environment-backed secrets, and never commit production secrets.
Terminate HTTPS securely: Use HTTPS at the server or trusted reverse proxy, and avoid exposing clear HTTP traffic to production clients.
Do not confuse Rails with Rack: Rails provides routing, controllers, Active Record, migrations, jobs, and views; Rack provides the underlying web-server interface.
57
What is the recommended way to observe and monitor a Rails 8 application while it is running in production?
KnownRevise
Monitor a Rails 8 application with a production observability stack that combines application logs, metrics, traces, health checks, dependency monitoring, and actionable alerts. No single signal is sufficient: logs help explain individual failures, metrics reveal trends, and traces show where a slow request spent its time.
Read full answerHide full answer
Applies toRails 8
Monitoring Layers
A production setup should monitor several layers:
Web requests: request rate, HTTP status codes, exception rate, and p50/p95/p99 latency.
Rails processes: CPU, memory, thread or fiber saturation, garbage collection, and unexpected restarts.
Background jobs: queue depth, oldest queued job age, processing time, retries, and failed jobs.
Caching and WebSockets: cache hit rate, cache latency, Solid Cable connection counts, and broadcast failures.
External dependencies: availability, timeout rate, and latency for payment, email, search, and other APIs.
The goal is not merely to collect telemetry, but to define alerts tied to user impact and an operational response.
Rails 8 Signals
Rails emits instrumentation through Active Support Notifications. Important events include:
process_action.action_controller for controller requests
perform.active_job for background-job execution
sql.active_record for database queries
Action View rendering events for template performance
Cache instrumentation such as cache_read.active_support
Rails also provides Rails.error (introduced in Rails 7.0 and current in Rails 8) as the unified error-reporting API. Report exceptions with Rails.error.report or subscribe with Rails.error.subscribe to forward them to your observability service.
These events can be consumed by an observability agent or subscribed to directly. An APM or OpenTelemetry integration is usually preferable to logging every event, especially for SQL and rendering instrumentation, because tracing can retain the relationships among a request, its database calls, and its jobs.
Rails 8 made Solid Queue, Solid Cache, and Solid Cable the default for new applications, replacing the previous external defaults. Monitor the queue and cache systems as part of the application, not as invisible infrastructure.
Health Checks and Dependencies
Use different checks for different purposes:
Liveness: answers whether the Rails process is running and can respond to HTTP; keep it independent of downstream dependencies so a database outage does not trigger restarts.
Readiness: verifies whether the instance can safely receive traffic, including required dependencies such as the primary database.
Dependency checks: separately measure queues, caches, and external services.
Rails 8 provides the application health route introduced in Rails 7.1, commonly exposed as /up through rails/health#show. This check verifies that the application booted successfully; it does not by itself prove that every downstream dependency is healthy.
Return only a generic public response such as {"status":"ok"}. Record detailed exception information in protected logs. Do not expose connection strings, SQL, hostnames, or external-service responses.
Metrics, Logs, and Alerting
Send production logs to a centralized collector such as a log platform or observability service. Structure logs so they can be filtered by request ID, controller, job ID, status, and latency. Avoid logging request parameters indiscriminately because they may contain credentials, tokens, or personal information.
Define alerts using symptoms and thresholds rather than isolated infrastructure values. Useful alerts include:
Elevated 5xx rate over a sustained period
p95 request latency above the service objective
A growing Solid Queue backlog
Repeated background-job failures
Database connection-pool exhaustion
Cache or external-dependency failure rates
Repeated process restarts or memory growth
Each alert should link to a runbook describing investigation, mitigation, rollback, and escalation steps.
Best Practices
Correlate request logs, traces, metrics, and job identifiers with a shared request or trace ID.
Track deployment markers so alerts can be compared with recent releases.
Test health checks and alerts regularly by simulating database failure or deliberately delaying a dependency.
Keep high-cardinality values such as raw user IDs out of metric labels; put them in logs or traces instead.
Set retention, sampling, and redaction policies before sending telemetry to third-party services.
Monitor both the Rails application and its deployment platform, database, queue, cache, and external providers.
Use Rails.error to centralize error reporting instead of rescuing and logging exceptions ad hoc.
58
How can Rails Engines be used to organize a Rails application into independently maintainable modules?
KnownRevise
A Rails Engine packages part of a Rails application as a reusable, isolated module with its own routes, controllers, models, views, migrations, assets, and tests. It provides the same Rails framework capabilities as a conventional application while allowing multiple engines to coexist within one host application.
Read full answerHide full answer
Applies toRails 8
How It Works
A Rails Engine is a class inheriting from Rails::Engine. When loaded by a host application, it participates in Rails initialization, configuration, initializers, middleware, autoloading, and route recognition.
Most engines use isolate_namespace to:
Scope route helpers to the engine.
Namespace controllers, models, and other constants.
Prefix database table names by default.
Prevent identifiers from colliding with host-application classes.
Make helpers such as billing.invoices_path available instead of potentially conflicting generic names.
An engine can also be mounted at a URL path and reused in multiple Rails applications.
Main Components
A typical engine contains:
Engine class: Located under lib/<engine>/engine.rb, it inherits from Rails::Engine and declares its namespace.
Application code: Controllers, models, jobs, services, and views live under app/.
Routes: The engine defines endpoints in config/routes.rb.
Migrations: Engine-owned schema changes live under db/migrate.
Assets and tasks: Engine assets and Rake tasks can remain within the engine.
Tests: Engine behavior should be tested independently, in addition to host-application integration tests.
The public classes and services of an engine form its interface. The host application should use that interface rather than modifying engine internals directly.
Mounting and Reuse
Once the engine is included through the Gemfile, its routes are mounted in the host application:
# config/routes.rb in the host application
Rails.application.routes.draw do
mount Billing::Engine => "/billing"
end
Requests such as /billing/invoices are then handled by the engine. This approach works well for features with their own user-facing routes.
For internal modularization without separate URL endpoints, an engine can still provide models and services. However, a plain Ruby namespace may be simpler when independent routing, migrations, assets, and lifecycle behavior are unnecessary.
Approach
Best suited for
Trade-off
Mounted engine
Reusable feature with its own endpoints
Adds routing and namespace configuration
Internal engine
Shared domain logic with minimal routing
Requires careful separation of engine internals
Plain namespace
Small internal module
Less isolation and packaging
Code Example
Generate a mountable engine inside the host project:
rails plugin new --mountable engines/billing
Reference the local engine from the host application’s Gemfile:
gem "billing", path: "engines/billing"
Define the engine namespace:
# engines/billing/lib/billing/engine.rb
module Billing
class Engine < ::Rails::Engine
isolate_namespace Billing
end
end
Define the engine’s own routes:
# engines/billing/config/routes.rb
Billing::Engine.routes.draw do
root to: "home#index"
end
Implement a controller inside the engine’s namespace:
# engines/billing/app/controllers/billing/home_controller.rb
module Billing
class HomeController < ApplicationController
def index
render plain: "Billing engine"
end
end
end
Finally, mount it in the host application:
# config/routes.rb in the host application
Rails.application.routes.draw do
mount Billing::Engine => "/billing"
end
After adding the Gemfile dependency, run bundle install before starting the application.
Best Practices and Common Pitfalls
Use isolate_namespace: Without it, route helpers, table names, and constants may collide with the host application.
Keep engine interfaces narrow: Expose intentional services or domain objects instead of encouraging the host to modify engine internals.
Validate all input: Use Active Record validations and strong parameters within the engine; never interpolate user-provided values into SQL.
Prevent N+1 queries: Use includes, preload, or eager_load when controllers display associated records.
Manage migrations deliberately: Review an engine’s migrations, copy them with a task such as bin/rails billing:install:migrations, and then run bin/rails db:migrate.
Use main_app carefully: In engine routes and views, use main_app to access host-application route helpers; in other engine code, use Rails.application.routes.url_helpers when those helpers are required.
Avoid implicit dependencies: Shared models should be declared as explicit engine dependencies rather than assumed to exist.
59
Which widely adopted Ruby gems are commonly incorporated into Rails applications, and what purposes do they serve?
KnownRevise
Rails applications commonly use popular gems for authentication, authorization, background processing, testing, monitoring, administration, and payments. The appropriate choice depends on the application’s requirements; many Rails 8 capabilities are already built in or included in the framework’s default application structure.
Read full answerHide full answer
Applies toRuby on Rails 8.x
Rails-Provided Capabilities
Before adding third-party gems, check the functionality already included with Rails:
Active Storage: Handles uploaded files, image variants, and storage services such as S3.
Active Job: Provides a common interface for background jobs, including retries and scheduled execution.
Action Mailer: Manages transactional emails, previews, and delivery methods.
Solid Queue, Solid Cache, and Solid Cable: Official Rails components used by newly generated Rails 8 applications for job processing, caching, and real-time messaging. Solid Queue uses a relational database; generated production apps configure a separate queue database, while development uses the primary database.
Authentication generator: Introduced in Rails 8, it generates a basic authentication system based on has_secure_password. This is a starting point for new applications, although Devise remains widely used.
Common Application Gems
Gem
Primary purpose
When it is useful
devise
Authentication
Applications requiring sign-in, registration, password recovery, and account management
pundit
Authorization
Enforcing role- or record-based access policies
sidekiq
Background jobs
Redis-backed, high-throughput asynchronous processing outside the default Rails job backend; batching and advanced reliability features are available in Sidekiq Pro/Enterprise
activeadmin
Administration interfaces
Creating internal dashboards for managing application records
sentry-rails
Error monitoring
Reporting exceptions, request data, and performance information to Sentry
rack-attack
Request throttling
Limiting abusive or excessive requests at the Rack layer
stripe-ruby
Payment processing
Integrating Stripe Checkout, PaymentIntents, subscriptions, and webhooks
image_processing
Image transformations
Supporting Active Storage variants through ImageMagick or libvips
Authentication and authorization solve different problems: Devise authenticates users, while Pundit decides what an authenticated user may do.
Testing and Development Gems
Gem
Primary purpose
rspec-rails
Provides RSpec support for Rails examples, models, requests, and system tests
factory_bot_rails
Creates test data through reusable factories
capybara
Supports browser-driven system testing
shoulda-matchers
Adds concise validation and controller checks to RSpec or Minitest
faker
Generates realistic-looking test data
bullet
Detects N+1 query problems during development
brakeman
Scans the application for common security vulnerabilities
rubocop-rails-omakase
Applies Rails-oriented Ruby formatting and linting rules
These tools are usually development dependencies rather than runtime dependencies. A production deployment should not require test-framework or static-analysis gems unless it performs those tasks during its build process.
Choosing a Background Job Backend
Rails 8 applications generated with the standard defaults use Solid Queue in production, which stores jobs in a relational database (a separate queue database in the generated production configuration, and the primary database in development). This is a strong default because it reduces infrastructure requirements.
Use Sidekiq when Redis is already available or when the application needs its scheduling or throughput characteristics; batching and some advanced reliability features are part of Sidekiq Pro/Enterprise. Regardless of the backend, keep Active Job abstractions in the application layer so switching implementations does not require rewriting business logic.
Best Practices and Common Pitfalls
Avoid unnecessary dependencies: Active Storage, Action Mailer, and Active Job already cover many basic requirements. Every additional gem increases upgrade and security-maintenance work.
Use Rails credentials for secrets: Store secrets in encrypted Rails credentials (committing only the encrypted file, never the master key) or a managed secret store; never commit plaintext secrets.
Prefer the Rails 8 authentication generator for new applications: It is not a replacement for Devise, and either approach can be appropriate, but avoid combining both without a clear migration plan.
Keep authorization explicit: Authenticate the user with Devise or the generated authentication system, then enforce permissions independently with Pundit or application-specific policy objects.
Test background jobs: Execute jobs inline in tests where appropriate or assert that they were correctly enqueued.
Monitor dependencies: Review gem releases and security advisories regularly, but upgrade within the application’s supported Ruby and Rails versions.
Add gems only when needed: Payment, administration, throttling, and monitoring gems are common, but they are not appropriate for every application merely because they are popular.
60
Which version of Rails is current, and which capabilities does it add?
KnownRevise
Rails 8 is the current stable Rails line, with Rails 8.0 released in November 2024 and followed by 8.x point releases. Its new-application defaults emphasize database-backed infrastructure and modern deployment tooling: Solid Queue, Solid Cache, Solid Cable, Propshaft, and Kamal 2 with Thruster. These Solid gems provide alternatives to Redis-backed services rather than prohibiting their use.
Read full answerHide full answer
Applies toRails 8.0 and later 8.x releases, running on Ruby 3.2 or newer.
How the Release Fits Into Rails History
Rails 7.0 made Hotwire—Turbo and Stimulus—and import maps the default front end, so a new application needs no Node.js for its default setup. Rails 8 retains those defaults.
Rails 8.0 introduced new-application defaults centered on Solid Queue, Solid Cache, Solid Cable, Propshaft, and container-oriented deployment tooling.
The generated container image supports web and background-worker roles. These normally run as separate containers or processes, even though they use the same image; Solid Queue still needs workers to execute jobs.
Point releases add smaller refinements. Consult the release notes for the exact patch version being deployed because behavior and defaults can change.
The Solid Stack
Solid Queue: The default Active Job backend in new Rails 8 applications. Jobs, retries, scheduled tasks, and recurring tasks are stored in a relational database and claimed by worker processes. Because execution still requires workers, restarting the web process alone does not replace those workers.
Solid Cache: A disk-backed cache store. It can hold substantially more data than an in-memory cache, although reads may be slower than with memory-only stores.
Solid Cable: An Action Cable adapter that persists broadcasts in a relational database, removing the need for a separate Redis pub/sub service when it is selected.
All three use Active Record and can use the primary database or a separate one configured through connects_to. The generated config/database.yml includes separate cache, queue, and cable database entries, but those databases are used only when the corresponding connections are configured. bin/rails db:prepare prepares the configured databases and their migrations.
Defaults in a New Application
Concern
Typical Rails 7 application
New Rails 8 application
Active Job backend
Async adapter in-process unless another backend was configured
Solid Queue
Cache store
File store under tmp/cache
Solid Cache
Action Cable
Commonly Redis in production
Solid Cable
Asset pipeline
Sprockets
Propshaft
JavaScript
Import maps; no Node.js required
Import maps; no Node.js required
Deployment
Application-specific PaaS, Capistrano, or container setup
Generated Dockerfile with Kamal 2 and Thruster
Authentication
Added separately, often with Devise
bin/rails generate authentication
Authentication, Rate Limiting, and Security
Authentication generator: bin/rails generate authentication creates a User model using has_secure_password, a Session model, user and session controllers, a passwords controller, a password-reset mailer, and associated views. It uses authenticate_by, introduced in Rails 7.1, to authenticate with constant-time password comparison.
Rate limiting: The rate_limit controller macro is new in Rails 8. For example, rate_limit to: 10, within: 3.minutes, only: :create limits the action and returns 429 Too Many Requests when exceeded. It uses the application’s configured cache store, which is Solid Cache in the standard new application.
Credentials: Use bin/rails credentials:edit and Rails.application.credentials. Rails.application.secrets was deprecated in Rails 7.1 and removed in Rails 8.0; the old secrets.yml workflow should not be used.
Production database: SQLite is a reasonable choice for applications with modest concurrency because Rails 8’s Solid services support it. PostgreSQL or MySQL is generally preferable for sustained concurrent writes.
Best Practices and Pitfalls
Run bin/rails app:update when upgrading and review the resulting defaults, including config.load_defaults 8.0.
Keep Redis-backed services when the application already operates them or requires their particular performance and scaling characteristics. The Solid gems are defaults, not mandatory replacements.
Size every database pool for the concurrency expected in one process. Because each web, worker, or other application process has its own pool, calculate total potential connections as pool size multiplied by the number of processes, and budget each separate database independently.
Verify the Ruby version before upgrading: Rails 8 requires Ruby 3.2 or newer.
Propshaft is the pipeline for new applications, but Sprockets remains supported, so existing applications do not have to migrate to upgrade Rails.
61
What sources and practices do you use to keep pace with new Rails releases, security fixes, and framework changes?
KnownRevise
Staying current with Rails requires following official release information, reviewing framework changes, and testing upgrades systematically rather than immediately deploying the newest version. The most reliable sources are the Rails release process, the official documentation, and the rails/rails repository.
Read full answerHide full answer
Applies toRails 8
Sources to Monitor
Rails blog and official release announcements: Use these to learn about major releases, new defaults, deprecations, and important bug fixes.
Rails Guides: Check the release notes, upgrading guide, API documentation, and security guidance for the Rails version you use.
rails/rails on GitHub: Review release tags, commits, pull requests, and discussions when a change may affect your application.
RubyGems and Bundler information: Monitor releases of rails, activerecord, actionpack, activesupport, and other framework components.
Security advisories: Track official Rails security announcements and advisories published through recognized vulnerability databases.
Community sources: Blogs, conference talks, and maintainer discussions can provide useful context, but verify their claims against official documentation.
A Safe Upgrade Workflow
Identify the affected release: Read the release notes and upgrading guide before changing dependencies.
Check compatibility: Confirm the required Ruby version, database adapter, deployment environment, and important gem compatibility.
Create an upgrade branch: Keep framework upgrades separate from unrelated feature work.
Update Rails: Change the Rails dependency, run Bundler, and commit the resulting Gemfile.lock.
Apply framework configuration changes: Use bin/rails app:update to apply relevant configuration changes from newer application templates.
Prepare the database: Run migrations and verify that indexes, constraints, and backfills work with production-like data volumes.
Run the full test suite: Include unit, integration, system, mailer, job, and background-worker tests.
Deploy gradually: Validate in staging, monitor application logs and performance, and retain a tested rollback procedure.
A ~> 8.0 Gemfile requirement permits compatible releases within Rails 8 while preventing an automatic upgrade to Rails 9. Narrow the requirement further if your team wants to remain on a particular minor or patch line.
Changes to Review Closely
Framework defaults: Rails 8 generated applications use Solid Queue, Solid Cache, and Solid Cable by default. Verify worker, cache, and real-time communication behavior after adopting or upgrading those components.
Authentication: The Rails 8 authentication generator provides a current baseline, but existing applications should still evaluate password policies, session handling, account recovery, and multi-factor authentication requirements.
Front-end behavior: Hotwire has been the default interactive stack since Rails 7. Review Turbo navigation, Turbo Frames, Turbo Streams, and Stimulus behavior when upgrading.
Active Record: Examine query changes, validation behavior, encryption features, transaction handling, and migration requirements.
Security fixes: Do not assume a patch release is routine; review the associated advisory and regression-test the affected behavior.
Keeping Up Without Constantly Upgrading
Follow stable releases, but avoid running Rails main in production.
Read release notes when a new Rails version or significant gem update appears.
Run bundle outdated periodically to identify dependency drift.
Enable automated dependency-update pull requests, such as GitHub Dependabot, but require CI and human review before merging.
Subscribe to release announcements and configure monitoring for deprecation warnings.
Keep the application on a supported Ruby and database release; Rails 8 requires Ruby 3.2 or newer.
Test against production-like data and infrastructure, since background workers, caching, cable adapters, and migrations often reveal issues that unit tests miss.
Avoid upgrading Rails and several unrelated dependencies in the same pull request. Smaller changes make regressions easier to identify and rollback safely.
62
Which resources can developers use to learn current Ruby on Rails best practices?
KnownRevise
The best starting points are the official Ruby on Rails Guides and, for a guided project, the Ruby on Rails Tutorial, complemented by the community Ruby style guides and the RuboCop configuration that new Rails applications ship with. Together they cover current conventions for application structure, Active Record, controllers, testing, security, and code style.
Read full answerHide full answer
Applies toRuby on Rails 8
Primary Resources
Ruby on Rails Guides: The canonical reference for Rails 8. Start with “Getting Started with Rails,” then study “Active Record Basics,” “Action Controller Overview,” “Testing Rails Applications,” and “Securing Rails Applications.” The guides are updated alongside Rails and are the most reliable source for current recommended APIs.
Ruby on Rails Tutorial: A project-based introduction that develops a Rails application while introducing routing, controllers, models, forms, authentication, Active Record, and testing. Its current edition targets Rails 7, so treat it as a guide to concepts and workflow and verify Rails 8 specifics against the official guides.
rubocop-rails-omakase: The RuboCop configuration maintained by the Rails project and included by default in newly generated Rails 8 applications (“omakase Ruby styling for Rails”). It encodes the formatting, naming and layout conventions Rails itself uses, so it is the machine-checked companion to the prose guides.
Testing Resources
Minitest documentation: Rails applications use Minitest by default. Learn the Rails testing guide alongside Minitest documentation, paying particular attention to unit tests, integration tests, system tests, fixtures, assertions, and isolated test databases.
Testing Rails Applications guide: Explains Rails-specific testing behavior that does not come directly from Minitest, including transactional tests, system testing, parallel testing, and test helpers.
Fixtures documentation: Learn how Rails fixtures represent test data and how associations are declared. For larger or more varied data sets, many teams add the third-party FactoryBot gem; use such factories deliberately rather than to hide test setup.
Security and Design Resources
Securing Rails Applications: The official guide covers mass assignment, SQL injection prevention, session management, cross-site scripting, cross-site request forgery, and other Rails security concerns.
Active Record guides: “Active Record Basics” plus the dedicated “Active Record Validations,” “Active Record Callbacks,” and “Active Record Query Interface” guides cover associations, scopes, transactions, and query behavior. Pay special attention to includes or preload for preventing N+1 queries.
Action Controller Overview: Learn controller responsibilities, parameter handling, sessions, cookies, filters, and rendering conventions. Prefer params.require(...).permit(...) for user-supplied input.
Complementary Ruby Resources
Ruby documentation: Consult the language documentation when a Rails question depends on Ruby syntax, blocks, enumerable methods, exceptions, or object-oriented Ruby behavior.
Ruby Style Guide: The community guide maintained alongside RuboCop provides broader conventions for Ruby code. Apply it with the omakase config where the two overlap, and prioritize Rails-specific guidance for Rails files.
RuboCop: The linter that runs both configurations. Teams should review and customize any configuration committed to a shared repository instead of editing generated defaults ad hoc.
Recommended Learning Approach
Follow the Rails Tutorial to understand the framework’s basic workflow, checking Rails 8 differences in the guides.
Use the official guides as the authoritative reference while building an application.
Enable rubocop-rails-omakase in the project’s .rubocop.yml so style is checked automatically.
Review the testing and security guides before deploying an application.
Keep the application’s Ruby and Rails dependencies current, because Rails 8 conventions differ from those in older Rails versions and tutorials.
63
How does Rails version its releases, and what does the release cycle look like?
KnownRevise
Rails uses Semantic Versioning with MAJOR.MINOR.PATCH numbers, plus prerelease and occasional four-component security-release forms. Major releases are generally annual, while additional minor series may ship between them under a schedule published by the Rails team rather than a fixed calendar.
Read full answerHide full answer
Applies toRails 8.x
Version Numbers
MAJOR: Introduces incompatible changes and is therefore named x.0, such as 7.0 or 8.0. A major release can raise minimum-version requirements and change framework defaults. Rails 8.0 requires Ruby 3.2.0 or newer, uses Solid Queue, Solid Cache, and Solid Cable by default in newly generated applications, and adds the authentication generator. Rails 7.0 made Hotwire—Turbo and Stimulus—the default front-end stack.
MINOR: Adds backward-compatible features and deprecations, such as 7.1 or 7.2. Breaking incompatibilities belong in a major release rather than a minor release.
PATCH: Releases bug fixes within an existing series, such as 8.0.1 or 8.0.2. Rails does not publish a separate patch for every reported bug.
Prereleases: Rails commonly publishes beta and release-candidate gems such as 8.0.0.beta1 and 8.0.0.rc1. They can be installed with gem install rails --pre or an explicit prerelease version in a Gemfile.
Security releases: Rails sometimes uses a fourth version component, such as 7.0.4.1, for an out-of-band security release that can be adopted without unrelated changes.
Edge: The main branch of rails/rails contains unreleased work. Guides marked “edge” therefore describe behaviour that has not necessarily shipped in a stable release.
The Release Cycle
Work is developed and reviewed on main.
Prerelease gems are published so applications can test an upcoming release; beta and release-candidate versions are typical, and alpha versions may also be used.
The final x.y.0 release is published with release notes. Major releases also receive a dedicated upgrade guide.
Selected bug-fix and security patch releases follow when appropriate. New features do not enter an already released series; they target the next minor or major series.
As new series are released, older series move through security-only maintenance and eventually reach end of life.
There is no permanently fixed release date. Recent major releases have generally arrived annually, while the Rails team may schedule additional minor releases between them.
Support Policy and Backports
Rails treats the latest minor series as the regular bug-fix series and normally keeps the two preceding minor series in security-only support. Older series are end-of-life, although exceptional critical security fixes may still be issued.
Release series
New features
Bug fixes
Security fixes
Latest released minor series
No; the series is feature-frozen
Yes
Yes
Two preceding supported series
No
Generally no
Yes
End-of-life series
No
No
Only exceptional critical fixes
Features are not normally backported. Selected fixes may be backported to supported stable branches, so the presence of a fix on main does not by itself tell you whether your installed version contains it.
Upgrading an Application
Read the release notes and upgrade guide for every major version crossed, and normally upgrade no more than one minor series at a time.
Update to the latest patch for the series currently in use before changing series.
Run bin/rails app:update to refresh framework configuration. For Rails 8.0, this can create config/initializers/new_framework_defaults_8.0.rb.
Ensure config.load_defaults 8.0 in config/application.rb reflects the defaults you intend to use; keeping an older value preserves the earlier framework defaults.
Treat deprecation warnings as upgrade work, but use each warning and the release notes to determine its removal version rather than assuming a fixed deprecation lifetime.
Inspect the running version with Rails.version, or compare versions using Rails.gem_version.
Common Pitfalls
Loose versus tight constraints: gem "rails", "~> 8.0" permits compatible 8.x upgrades, while gem "rails", "~> 8.0.0" remains within the 8.0.x patch series.
Relying on edge behaviour: Software installed from main can change without notice and is outside the stable release support policy.
Assuming every fix reaches every version: Check the specific release series and its release notes.
Confusing an emergency security release with normal patch cadence: A four-component version is primarily intended to deliver a security fix promptly; consult its release notes rather than treating it as a replacement for a planned minor upgrade.
64
In what ways does the Ruby runtime version influence the behavior, compatibility, and deployment of a Rails application?
KnownRevise
The Ruby runtime version determines which language features, standard-library APIs, dependencies, and performance characteristics are available to a Rails application. It is a compatibility constraint managed separately from the Rails version.
Read full answerHide full answer
Applies toRails 8 on Ruby 3.2.0 or newer
Compatibility Baseline
Rails 8 requires Ruby 3.2.0 or later. The Ruby version is usually declared in the Gemfile, while .ruby-version commonly records the exact version expected by version managers and deployment tools.
The Ruby requirement and Rails requirement control different things:
ruby ">= 3.2.0" establishes a minimum compatible Ruby version.
gem "rails", "~> 8.0.0" is narrower and permits only Rails 8.0.x releases.
gem "rails", github: "rails/rails" tracks the Rails repository and is not appropriate for a normal production application.
When the Gemfile declares a Ruby requirement, Bundler can record it in the RUBY VERSION section of Gemfile.lock.
Effects on the Application
Language features: Newer Ruby releases may provide syntax and standard-library APIs unavailable in older releases. Applications can use them only after updating their required Ruby version.
Framework behavior: Changing Ruby alone does not upgrade Rails or enable Rails features. Rails APIs, configuration defaults, and generators are controlled by the Rails version.
Dependency compatibility: Every gem can define its own required_ruby_version. A gem that supports Ruby 3.2 may not support Ruby 3.4 if it uses changed internal APIs.
Native extensions: Gems containing C or Rust code must be rebuilt for the selected Ruby version and operating system. Precompiled gems may not exist for every Ruby, operating system, and CPU architecture combination.
Performance: Ruby releases can change garbage collection, JIT behavior, and interpreter performance. These effects should be benchmarked using representative requests and jobs rather than assumed.
Security: Ruby patch releases contain interpreter and standard-library security fixes. Rails and Ruby security maintenance are separate concerns, so both must be kept current.
Development tooling: Bundler, Rails, bootsnap, debuggers, and editor integrations must all support the selected Ruby version. The same Ruby patch version should be used in development, CI, staging, and production.
Upgrading Ruby
Check .ruby-version, the Gemfile, Gemfile.lock, CI configuration, deployment images, and every native dependency.
Select a currently supported Ruby patch release that satisfies the Rails and gem requirements; the Rails minimum version is not necessarily the best production choice.
Review the Ruby release notes and the compatibility notes for Rails and critical gems.
Update every environment to the same Ruby version, reinstall dependencies, and run the complete test suite.
Test database migrations, background jobs, mail delivery, file handling, and other features that depend on native extensions.
Keep a Ruby-only upgrade separate from a Rails upgrade when possible, making failures easier to diagnose.
Changing the Ruby version can cause Bundler to resolve different dependency versions if a locked gem no longer supports the new runtime. Review and commit any resulting Gemfile.lock change deliberately rather than updating all dependencies without a reason.
Common Pitfalls
Using different Ruby versions across environments: A successful local run does not guarantee production compatibility.
Assuming Rails support implies gem support: Individual dependencies may impose stricter Ruby constraints.
Selecting only the minimum Ruby version: A minimum requirement describes compatibility, not the recommended operating version.
Ignoring native dependencies: An upgrade may require compiler tools, source builds, or platform-specific gem releases.
Upgrading Ruby and Rails simultaneously: Combining both changes increases the number of possible failure causes.
Treating a Ruby upgrade as an application feature upgrade: New Ruby behavior should be adopted deliberately, with tests and application changes where appropriate.
65
Which Ruby-based web frameworks can developers choose instead of Rails, and how do their designs and ideal use cases differ?
KnownRevise
Ruby web frameworks other than Rails provide alternatives ranging from lightweight Rack applications to structured, full-stack frameworks. The best choice depends on whether the application needs routing conventions, an ORM, background jobs, caching, real-time features, or simply a small HTTP interface.
Read full answerHide full answer
Applies toRuby 3.4, Rails 8, Sinatra 4.x, Hanami 2.x, Roda 3.x, Grape 2.x, and Rack 3.x
Main Alternatives
Framework
Main design
Best suited for
Sinatra
Small, modular Rack-based DSL
Microservices, prototypes, small APIs, and simple web applications
Hanami
Full-stack framework with explicit application structure
Medium-sized applications that benefit from clear architecture without Rails conventions
Roda
Routing built around an immutable tree of matchers and middleware
High-performance services and applications requiring precise control over request handling
Grape
DSL for describing and validating APIs
RESTful API services with structured parameters, serializers, and error handling
Rack
Low-level interface for Ruby web servers and middleware
Custom frameworks, API gateways, or applications requiring minimal dependencies
Framework Profiles
Sinatra: Routes are defined with a concise DSL, while extensions provide functionality such as sessions, authentication, and database integration. It offers much more freedom than Rails because it does not prescribe models, controllers, or application organization. It is a strong choice when dependency count and deployment simplicity matter.
Hanami: Provides a complete application structure, dependency injection, routing, controllers, and an ORM-oriented approach. It is more comparable to Rails than Sinatra is, but it emphasizes explicitness and architectural boundaries. Hanami is appropriate when a team wants a full-stack framework without adopting Rails’ conventions.
Roda: Treats routing as a tree of matchers, where each route is accompanied by nested middleware. This produces predictable request processing and allows features such as authentication or logging to be scoped to particular route branches. It is popular for services that need fine-grained control.
Grape: Focuses on API contracts rather than server-rendered HTML. It supports parameter validation, versioning, formatting, and structured errors. It can be mounted inside a Rack application, including a Rails or Hanami application.
Rack: Is the underlying interface between Ruby web servers and applications. Rack itself is not a full-stack framework, but many alternatives are built on it. Use Rack directly when the application needs only a small number of endpoints or custom middleware behavior.
Choosing an Alternative
Choose Sinatra for a small service, prototype, or application where rapid setup and simplicity are more important than integrated conventions.
Choose Hanami when a larger application needs architectural structure but the team does not want Rails’ convention-heavy design.
Choose Roda when routing control, middleware composition, and performance are central requirements.
Choose Grape when the primary deliverable is a well-defined API rather than an HTML application.
Choose Rack when building a custom framework or composing specialized middleware.
Stay with Rails when the application benefits from Active Record, generators, conventions, background jobs, caching, and an integrated development workflow.
Rails 8 Context
Rails 8 remains the broadest option for conventional Ruby web applications. It introduced Solid Queue, Solid Cache, and Solid Cable as the default production adapters for jobs, caching, and real-time communication, and added an authentication generator. Hotwire became the default interactivity stack in Rails 7, rather than Rails 8.
For an API-only product, Rails can also be configured without server-rendered HTML and Action View. This is not a separate framework, but it is often preferable to introducing another framework when the team still wants Active Record, authentication generators, and Rails tooling.
Migration and Interoperability
Moving away from Rails is rarely an all-or-nothing decision. Rack applications can coexist behind one reverse proxy, and components such as Active Record, Sequel, Sidekiq, or Active Job can be reused where appropriate. The main trade-off is that alternatives generally require teams to assemble and maintain more infrastructure themselves, particularly persistence, job processing, caching, testing helpers, and application structure.
66
In a Rails 8 application, how can React or Vue.js be used as the frontend while Rails continues to provide the backend API?
KnownRevise
Rails integrates with React and Vue.js through a JSON API architecture: Rails manages persistence, validation, authentication, and business rules, while the frontend handles rendering, client-side routing, and user interactions. In production, a reverse proxy can serve both applications under one origin; during development, the frontend development server usually proxies API requests to Rails.
Read full answerHide full answer
Applies toRails 8; React 19 and Vue 3.5 as separate single-page frontends
Recommended Architecture
A typical request flow is:
A React or Vue component sends an HTTP request to a Rails endpoint.
Rails authenticates the request and applies strong parameters.
Active Record retrieves or changes the required records.
Rails returns a JSON response with an appropriate HTTP status.
The frontend updates its state and renders the result.
Rails can be created as an API-focused application with rails new my_api --api, although a standard Rails application can also expose JSON endpoints from selected controllers. The important boundary is that database models should not be treated directly as public API contracts.
Backend Responsibilities
Rails should remain responsible for server-authoritative behavior:
Persistence: Use Active Record for database operations.
Validation: Define model and service-object validations rather than trusting frontend validation.
Authorization: Verify that the current user may access or modify each resource.
Serialization: Return explicit JSON representations instead of exposing raw model attributes unintentionally.
Error handling: Return consistent error structures and appropriate status codes such as 422 for invalid input.
Strong parameters prevent unexpected attributes from being assigned. For example, a permitted user profile update might allow name and email, but not admin or account_balance.
When rendering associations, use includes, preload, or eager_load as appropriate. Without this, a page that displays many records and their associated comments can produce an N+1 query problem.
Frontend Responsibilities
React and Vue own browser-specific concerns:
Component composition and reactive state
Client-side routing
Form state and immediate validation feedback
Loading, empty, and error states
API-client organization and request cancellation
Optimistic or pessimistic update strategies
The frontend may improve usability with immediate validation, but Rails must repeat every security-critical validation. Similarly, hiding a button in React or Vue is not authorization; Rails must enforce permissions when handling the request.
A practical frontend API layer should centralize the base URL, authentication headers, JSON parsing, and normalized error handling. This prevents every component from implementing a slightly different fetch or HTTP-client configuration.
Authentication and Security
For a same-origin deployment, cookie-based sessions are usually straightforward. Rails manages the session, while the browser sends the session cookie with each request. Rails 8 introduced an authentication generator, which can provide a starting point for password-based session authentication, but its generated flows must still be reviewed and extended for the application’s registration, recovery, and authorization requirements.
When using cookies:
Serve everything over HTTPS.
Use HttpOnly, Secure, and an appropriate SameSite policy for session cookies.
Keep Rails CSRF protection enabled for session-authenticated state-changing requests.
Have the frontend obtain and send the CSRF token using the configured request header.
If the frontend and Rails run on different origins, configure CORS with an explicit allowlist. Wildcard origins should not be combined with credentialed requests. Depending on the deployment, CORS may be handled by Rails middleware such as rack-cors or by the reverse proxy. Rails should never expose secrets, database credentials, or unrestricted mass assignment through JSON endpoints.
Deployment and Framework Choice
Approach
Best fit
Main tradeoff
Separate React or Vue SPA plus Rails API
Highly interactive, application-like experiences
Requires an explicit client/server API contract
Rails-rendered pages with React or Vue components
Selective interactive widgets within server-rendered pages
More integration complexity
Rails with Hotwire
Most CRUD workflows and server-rendered applications
Less suitable when the frontend requires extensive client-side state or routing
During development, run Rails and the frontend development server separately and proxy /api requests from the frontend server to Rails. This keeps requests same-origin from the browser’s perspective and avoids unnecessary CORS configuration.
In production, a reverse proxy can route /api to Rails and serve the compiled React or Vue assets from a CDN or static-file service. Keep API versioning and response contracts stable so frontend deployments do not unexpectedly break when Rails internals change.