Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 082 / 100

Rails interview questions: practical tips and trade-offs

Questions that show experience.

Expona Admin7 min read

Rails interview questions: practical tips and trade-offs

When you're interviewing for a senior Rails role, the questions rarely focus on syntax or Rails "magic." Instead, they probe your understanding of real-world constraints: database performance, security vulnerabilities, and the decisions you've made under pressure. We've seen these same patterns across e-commerce platforms, CRM systems, and trading applications—and the engineers who answer well aren't just technically sound, they're pragmatic about trade-offs.

How do you avoid N+1 queries?

This is the first question many interviewers ask, and for good reason. N+1 queries are a silent killer in production. You write code that works fine with 10 users, then deploy and watch your database connections max out at scale.

The classic scenario: you fetch a list of orders and then, in your view or serializer, access the associated user for each one. That's one query for orders, then one query per order to fetch the user. With 1,000 orders, you're suddenly running 1,001 queries.

There are three main approaches, and they have different trade-offs:

Using includes

includes lazy-loads associations using a second query (or JOIN, depending on context). Rails figures out the best strategy for you.

ruby
# Instead of:
orders = Order.all
orders.each { |o| o.user.name }  # N+1 queries

# Use:
orders = Order.includes(:user)
orders.each { |o| o.user.name }  # 2 queries total

Trade-off: includes is flexible and handles most cases, but it can hide complexity. It'll use a second query by default, or a LEFT OUTER JOIN if you chain other conditions. For deeply nested associations, you might end up fetching more data than you need.

Using preload

preload always uses a separate query per association level. It's explicit about its cost.

ruby
orders = Order.preload(:user, :items)
# Two separate queries: one for orders, one for users, one for items

Trade-off: You know exactly what queries will run, but you lose the option of doing a single JOIN. For large datasets, this can mean fetching the same user data multiple times.

Using strict_loading

This is the newer, stricter approach. Enable it in development and test to catch N+1 problems before they reach production.

ruby
orders = Order.strict_loading.includes(:user)
# If you later access order.items (not preloaded), it raises an error

# Or enable it globally for a model:
class Order < ApplicationRecord
  has_many :items

  def self.default_scope
    strict_loading
  end
end

Trade-off: Strictness helps catch bugs, but it can be noisy in large legacy codebases. You need to use it intentionally, typically in API responses or high-traffic queries, not everywhere.

Practical guidance: Start with includes for 80% of cases. Use preload when you need explicit control. Enable strict_loading on critical paths in your API controllers.

What are strong parameters?

Strong parameters are Rails's built-in defense against mass assignment vulnerabilities. They whitelist the exact attributes you allow from user input before they hit your model.

Without them, an attacker could craft a request with extra parameters and modify fields they shouldn't touch.

ruby
# Vulnerable (Rails < 4):
def create
  @user = User.create(params[:user])
  # If params contains is_admin: true, the attacker wins
end

# Protected (Rails 4+):
def create
  @user = User.create(user_params)
end

private

def user_params
  params.require(:user).permit(:name, :email, :password)
  # is_admin is rejected silently
end

The require ensures the parameter exists. The permit lists allowed keys. Nested attributes and arrays are supported:

ruby
def order_params
  params.require(:order).permit(:customer_name, items: [:product_id, :quantity])
end

Common mistake: Using permit! to allow everything. That defeats the purpose. If you're tempted, it usually means your API design isn't clear.

Practical guidance: Strong parameters should be your first line of defense, but not your last. Pair them with database-level constraints and clear authorization logic. In Rails 7+, consider using permit_all_parameters only in development.

How do background jobs work?

Synchronous code blocks the request. If you're sending an email or processing an image in your controller, the user waits. Background jobs move that work off the main thread.

Rails provides Active Job, an abstraction layer over multiple adapters. In development, you might use the inline adapter (runs immediately). In production, you'd use Sidekiq, Solid Queue, or another worker system.

ruby
# Define a job:
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome_email(user).deliver_later
  end
end

# Enqueue it:
SendWelcomeEmailJob.perform_later(user.id)
# Or schedule for later:
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)

Key principles:

  • Pass IDs, not objects. If you serialize a user object and the user record changes before the job runs, you're working with stale data. IDs are cheap to store and fetch fresh data inside the job.
  • Jobs must be idempotent. If a job fails and is retried, it should produce the same result. Don't increment a counter in a job; set it instead.
  • Monitor job queues. In production, a backed-up queue means users aren't getting emails or notifications. Set up alerting.

Trade-offs between adapters:

Adapter Pros Cons
Inline No extra infrastructure, good for tests Blocks the request, no retries
Sidekiq Fast, reliable, mature ecosystem Requires Redis, adds operational complexity
Solid Queue (Rails 8+) No external dependency, database-backed Newer, fewer third-party tools
Delayed Job Simple, database-backed Slower for high throughput

Practical guidance: Start with Sidekiq if you're building a new production app. It's battle-tested across e-commerce and SaaS. Consider Solid Queue if you want to avoid the Redis dependency. Always write jobs that can be safely retried.

How do you secure a Rails app?

Rails ships with several built-in protections—CSRF tokens, SQL injection prevention through parameterized queries, and content security headers. But security isn't just about layers; it's about understanding what each one defends against.

CSRF tokens

A cross-site request forgery attack tricks a logged-in user into making an unwanted action on another site. Rails prevents this by requiring a token in any state-changing request (POST, PUT, DELETE).

ruby
# In your form:
<%= form_with model: @order do |f| %>
  
  <%= f.text_field :description %>
  <%= f.submit %>
<% end %>

# In your controller, it's validated automatically

Gotcha: If you're building a JSON API, make sure you're not skipping CSRF protection for state-changing endpoints. Rails disables it by default for APIs, which is fine if you're using token-based auth (like JWT), but it's easy to miss.

SQL injection

Never interpolate user input into SQL strings. Always use parameterized queries.

ruby
# Vulnerable:
User.where("email = '#{params[:email]}'")

# Safe:
User.where(email: params[:email])
User.where("email = ?", params[:email])

Authorization and authentication

Rails doesn't do this for you. You need to implement it (or use a gem like Devise for auth, Pundit for authorization).

ruby
# Example with Pundit:
def show
  @order = Order.find(params[:id])
  authorize @order  # Raises NotAuthorizedError if user can't access it
end

# Define policies:
class OrderPolicy
  def initialize(user, order)
    @user = user
    @order = order
  end

  def show?
    @user == @order.user || @user.admin?
  end
end

Scanning for vulnerabilities

Use Brakeman to scan your codebase for common issues:

ruby
gem 'brakeman', require: false
# Run: brakeman

It catches things like hardcoded secrets, unsafe redirects, and unescaped output. Run it in CI before every deploy.

Practical guidance: Security is layered. Use Rails's built-in protections, add tools like Brakeman, keep dependencies updated, and most importantly, implement clear authorization logic. The biggest vulnerabilities we've seen in production aren't subtle; they're missing permission checks.

Common mistakes

  • Treating N+1 queries as optional: Performance problems in interviews often hint at poor database discipline in production. Show you care about query efficiency.
  • Using permit! or skipping strong parameters: It's a red flag that you're not thinking about input validation.
  • Passing AR objects to background jobs: It works sometimes, but serialization and deserialization is fragile. Stick with IDs.
  • Assuming Rails handles security for you: Rails gives you the tools, but you have to use them correctly. CSRF and SQL injection protection only work if you follow the patterns.

Key takeaways

  • includes handles most N+1 problems; use strict_loading on critical paths and preload when you need explicit control.
  • Strong parameters are your first defense against mass assignment; pair them with database constraints and authorization.
  • Background jobs via Active Job should be idempotent and receive IDs, not objects; Sidekiq is mature and reliable; Solid Queue is a newer alternative without Redis.
  • Security is layered: use Rails's built-in protections, validate authorization in controllers, run Brakeman in CI, and keep dependencies updated.
  • When answering interview questions, acknowledge trade-offs. Show you've made real decisions under constraints, not just memorized the "right" answer.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles