Skip to content
Web and Mobile Development

Ruby on Rails interview questions and answers

66 questions, 18 with code. About 4 h 54 min to read them all.

By TestLab · Updated

Plan for an interview
66 questions

Track what you know and what to revise.

Sign in to track progress

You can open a few full answers without signing in. Sign in to read all

1

Can you define Ruby on Rails and outline the key factors that contribute to its widespread use in modern web application development?

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 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?

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 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?

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 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.

  • Consistent tooling: Generators, autoloading, testing, and deployment workflows follow familiar patterns.

Limitations and Common Pitfalls

  • 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?

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 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

Use the generated helpers in templates:

<%= link_to "Articles", articles_path %>
<%= link_to "About", about_path %>

Best Practices and Common Pitfalls

  • 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?

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 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.

  • Prefer explicit permit lists. Passing an entire parameter hash risks exposing protected attributes.

Common Pitfalls

  • 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?

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 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?

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 answer

Applies toRuby on Rails 8

Request Flow

  1. Rack middleware processes the request

  2. Rails receives the request as a Rack environment.

  3. Middleware can manage sessions, cookies, logging, compression, and other cross-cutting concerns.

  4. CSRF verification is primarily performed by controller protection rather than by ordinary Rack middleware.

  5. The router selects an endpoint

  6. The router matches the HTTP method and path.

  7. It resolves a route to a controller and action such as PostsController#show.

  8. Rails instantiates the controller, assigns the request and response objects, and makes route and request parameters available through params.

  9. Controller callbacks wrap the action

  10. before_action callbacks run before the action.

  11. around_action callbacks can execute setup, yield to the action, and perform cleanup.

  12. The action method runs when the callback chain reaches it.

  13. after_action callbacks run only when the chain completes without an exception or termination.

  14. The action creates a response

  15. An action can call render, redirect_to, or head.

  16. If the action has not already produced a response, Rails normally performs implicit rendering by finding a template matching the action.

  17. The resulting response includes its status, headers, body, cookies, and other metadata.

  18. The response returns through the stack

  19. The response travels back through the Rack middleware stack and is sent to the client.

  20. 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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:

article = Article.create!(
  title: "Building Rails applications",
  body: "An introduction to Rails models."
)

article.persisted? # => true

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?

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 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.

Running Migrations

The main commands are:

bin/rails generate migration CreateUsers
bin/rails db:migrate
bin/rails db:migrate:status
bin/rails db:rollback
bin/rails db:prepare

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?

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 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.

  • Association: validates_associated validates associated objects.

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`?

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 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?

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 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:

Event Callback sequence
Create before_validation, after_validation, before_save, around_save, before_create, around_create, after_create, after_save, around_save, after_commit
Update before_validation, after_validation, before_save, around_save, before_update, around_update, after_update, after_save, around_save, after_commit
Destroy before_destroy, around_destroy, after_destroy, 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:

  • before_save :normalize_email

  • after_create :send_welcome, if: :confirmed?

  • before_update :audit_changes, unless: :skip_auditing?

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?

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 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.

20

How do polymorphic associations operate in Ruby on Rails, and which design scenarios benefit most from this pattern?

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 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.

Database Schema Requirements

  • Reference columns: t.references :commentable, polymorphic: true creates commentable_id and commentable_type columns.

  • 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?

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 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?

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 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?

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 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.

Code Example

<!-- app/views/users/_user_card.html.erb -->
<div class="card">
  <h3><%= user.name %></h3>
  <p><%= user.email %></p>
</div>
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= render partial: "user_card", locals: { user: @user } %>

<h2>All Users</h2>
<%= render partial: "user_card", collection: @users, as: :user %>
# 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.

25

How does Webpacker integrate with a Rails application to compile, serve, and reference JavaScript assets?

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 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

  1. 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.

  2. 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.

  3. 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.

  4. 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?

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 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:

  • data-user-id becomes element.dataset.userId.

  • data-user-name-value becomes element.dataset.userNameValue.

Attribute values are strings in the DOM. A data-* attribute is metadata only; it is not automatically submitted with an HTML form.

Creating Them in Rails Views

Approach Example Result
Direct ERB <div data-user-id="<%= @user.id %>"> Writes the attribute explicitly
Rails tag helper tag.div data: { user_id: @user.id } Produces data-user-id="..."
Link helper link_to "Profile", user_path(@user), data: { user_id: @user.id } Adds the attribute to the generated link

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:

<%# app/views/users/show.html.erb %>
<%= tag.button "Show details",
      type: "button",
      data: {
        controller: "user-details",
        action: "click->user-details#show",
        user_id: @user.id,
        user_details_user_name_value: @user.name
      } %>

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?

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 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.

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 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?

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 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?

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 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.

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 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.

33

How are system tests defined and what steps are involved in adding them to a Rails application?

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 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

  1. 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.

  2. 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.

  3. Generate a test file: Run bin/rails generate system_test articles to create test/system/articles_test.rb.

  4. Write the user journey: Use Capybara’s DSL, including visit, fill_in, click_on, within, assert_text, and assert_selector.

  5. 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?

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 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?

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 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?

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 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:

  1. The user submits an email address and password.

  2. Devise’s database_authenticatable module verifies the credentials.

  3. The authenticated user identifier is stored in the Rails session.

  4. Subsequent requests can access current_user.

  5. Controllers use authenticate_user! to require authentication.

Devise uses the Rack-based Warden framework internally for authentication strategies and session management.

Setting Up Devise

Install the gem and run its generators:

bundle add devise --version "4.9.4"
bin/rails generate devise:install
bin/rails generate devise User
bin/rails db:migrate

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:

<% if user_signed_in? %>
  <%= link_to "Dashboard", dashboard_path %>
  <%= button_to "Sign out", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "Sign in", new_user_session_path %>
  <%= link_to "Sign up", new_user_registration_path %>
<% end %>

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:

  • database_authenticatable: Verifies email-and-password credentials.

  • registerable: Supports account creation.

  • recoverable: Provides password-reset instructions.

  • rememberable: Maintains a “remember me” session beyond the browser session.

  • validatable: Adds presence, uniqueness, length, and confirmation validations.

  • confirmable: Requires confirmation of email addresses.

  • lockable: Locks accounts after repeated failed attempts.

  • trackable: Records selected sign-in metadata.

  • timeoutable: Expires inactive authenticated sessions.

  • 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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?

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 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

  1. The router dispatches to a controller action.

  2. 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.

  3. The action builds permitted attributes with strong parameters, for example params.require(:article).permit(:title, :body).

  4. 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.

  5. 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.

  6. 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?

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 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?

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 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:

<%# app/views/users/show.html.erb %>
<h1><%= @user_decorator.display_name %></h1>
<p><%= @user_decorator.email %></p>
<span><%= @user_decorator.account_status %></span>

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?

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 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.

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 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?

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 answer

Applies toRails 8 with Ruby 3.x; version notes are called out where behaviour changed.

Levels of Modularization

Approach Isolation Typical use
Namespaced code (app/models/billing/, Billing::InvoicesController) Low — one app, one database 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?

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 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?

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 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

  1. extend ActiveSupport::Concern marks the module as a concern.

  2. Instance methods written in the module body become instance methods of the including class.

  3. included do ... end is evaluated with class_eval in the including class, so scope, validates, has_many and before_action register on that class.

  4. class_methods do ... end defines methods that are extended onto the including class.

  5. 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?

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.

Read full answer

Applies toRails 8 (Action Cable 8, turbo-rails 2, solid_cable 1)

How It Works

  • 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?

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 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?

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 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:

  1. A Rack-compatible server such as Puma receives the HTTP request.

  2. The server invokes the Rails Rack endpoint with a Rack environment.

  3. Rails middleware processes concerns such as host authorization, cookies, sessions, static files, security headers, and request identifiers.

  4. ActionDispatch::Request wraps the Rack environment and provides Rails-friendly access to parameters, headers, cookies, and the request body.

  5. The Rails router identifies the controller action.

  6. The controller and view generate the response.

  7. 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.

58

How can Rails Engines be used to organize a Rails application into independently maintainable modules?

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 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?

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 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.

  • Action Cable: Provides WebSocket-based communication.

  • 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?

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 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?

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 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

  1. Identify the affected release: Read the release notes and upgrading guide before changing dependencies.

  2. Check compatibility: Confirm the required Ruby version, database adapter, deployment environment, and important gem compatibility.

  3. Create an upgrade branch: Keep framework upgrades separate from unrelated feature work.

  4. Update Rails: Change the Rails dependency, run Bundler, and commit the resulting Gemfile.lock.

  5. Apply framework configuration changes: Use bin/rails app:update to apply relevant configuration changes from newer application templates.

  6. Prepare the database: Run migrations and verify that indexes, constraints, and backfills work with production-like data volumes.

  7. Run the full test suite: Include unit, integration, system, mailer, job, and background-worker tests.

  8. 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?

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 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

  1. Follow the Rails Tutorial to understand the framework’s basic workflow, checking Rails 8 differences in the guides.

  2. Use the official guides as the authoritative reference while building an application.

  3. Enable rubocop-rails-omakase in the project’s .rubocop.yml so style is checked automatically.

  4. Review the testing and security guides before deploying an application.

  5. 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?

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 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

  1. Work is developed and reviewed on main.

  2. 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.

  3. The final x.y.0 release is published with release notes. Major releases also receive a dedicated upgrade guide.

  4. 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.

  5. 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?

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 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" permits Rails 8.x versions but excludes Rails 9.

  • 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

  1. Check .ruby-version, the Gemfile, Gemfile.lock, CI configuration, deployment images, and every native dependency.

  2. 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.

  3. Review the Ruby release notes and the compatibility notes for Rails and critical gems.

  4. Update every environment to the same Ruby version, reinstall dependencies, and run the complete test suite.

  5. Test database migrations, background jobs, mail delivery, file handling, and other features that depend on native extensions.

  6. 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?

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 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

  1. Choose Sinatra for a small service, prototype, or application where rapid setup and simplicity are more important than integrated conventions.

  2. Choose Hanami when a larger application needs architectural structure but the team does not want Rails’ convention-heavy design.

  3. Choose Roda when routing control, middleware composition, and performance are central requirements.

  4. Choose Grape when the primary deliverable is a well-defined API rather than an HTML application.

  5. Choose Rack when building a custom framework or composing specialized middleware.

  6. 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?

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 answer

Applies toRails 8; React 19 and Vue 3.5 as separate single-page frontends

Recommended Architecture

A typical request flow is:

  1. A React or Vue component sends an HTTP request to a Rails endpoint.

  2. Rails authenticates the request and applies strong parameters.

  3. Active Record retrieves or changes the required records.

  4. Rails returns a JSON response with an appropriate HTTP status.

  5. 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.

Sign in to keep reading

You've opened 2 full answers. Sign in to read all 66 Ruby on Rails answers, mark what you know and pick up where you left off.

All Web and Mobile Development subjects