LearnThatStack Ace your next interview

Ruby on Rails

Rails 8. Older ways are marked. Checked Nov 2026 128 code snippets

The model

  • The controller takes each request, asks the models for data and picks a view to render. Models also hold the business logic. That split is MVC.
  • Convention over configuration means names do the wiring. You write config only when you break a convention.
  • Active Record wraps one database row in one object. Columns become attributes, because Rails reads the schema at runtime.
  • A relation is lazy. where runs no SQL until you use the records, so you can keep chaining.
  • Rails 8 needs no Redis by default. Solid Queue, Solid Cache and Solid Cable keep jobs, cache entries and broadcasts in the database.
Pumaapp serverRack middlewaresessions, cookiesRouterconfig/routes.rbControllerfilters, actionINrequestverb and pathfirst matchOUTHTML or JSONresponseEARLYfilter redirects
A request goes IN through middleware and the router to one action. The response comes OUT the same way. A middleware or a filter can answer EARLY.

App layout & conventions

where code lives and what the names do

Environments and config

Say what the three defaults dodevelopment reloads code, test has its own database, production caches.
Set the environment at startupRAILS_ENV=staging
Edit encrypted secretsbin/rails credentials:edit

Tipconfig/environments/ overrides config/application.rb for one environment.

TrapCommit credentials.yml.enc, but never config/master.key, which decrypts it.

Autoloading with Zeitwerk

Map admin/user.rb to its constantAdmin::User
Find files that break the naming rulesbin/rails zeitwerk:check

TipDevelopment reloads changed code. Production eager loads all of it at boot.

TrapInitializers must not use reloadable constants under app, because reloading replaces them.

Generators and engines

Generate a model, or a custom generatorbin/rails g model Userbin/rails g generator x
Create an engine, a miniature app in a gemrails plugin new e --mountable

TipAn engine shares a feature across apps. You must version it.

Routes & controllers

from a URL to a response

Request cycle and Rack middleware

List the stops a request makesWeb server, Puma, middleware, router, filters, action, view, then back.
List the stack, then add to itbin/rails middlewareconfig.middleware.use X

TipA middleware takes the next app in initialize, and its call(env) wraps @app.call(env).

Render, redirect and formats

Show the form again, errors keptrender :new, status: 422
After a save, send a 302 to a new pageredirect_to @user
Serve several formatsrespond_to do |format|

TipRails reads the format from the extension or Accept. An unlisted format answers 406.

TrapTurbo only shows a form response that redirects or carries an error status, so a failed save needs 422.

Strong parameters

Stop mass assignment with an allow listparams.require(:user).permit(:name)
Require and permit in one call (8.0+)params.expect(user: [:name])
Name what they replaced in Rails 4attr_accessible

TrapUnpermitted params raise ForbiddenAttributesError at the model. A missing require key returns 400.

Models & associations

map classes to tables and set their rules

Active Record and validations

Say what the Active Record pattern isOne object wraps one row, with its data and logic. The cost is coupling to storage.
Add a built-in rulevalidates :email, presence: true
Add a custom rulevalidate :my_checkerrors.add(:x, "bad")

Tipsave returns false on failure, and save! raises.

TrapParallel requests can both pass a uniqueness check, because it runs in Ruby. Add a unique index.

Associations

Hold the foreign keybelongs_to :user
Declare the other sidehas_one :profilehas_many :posts
Go through a join model, to many or to onethrough: :taggings
Link through a bare join tablehas_and_belongs_to_many :tags
Belong to several kinds of modelpolymorphic: true

Tipbelongs_to checks that the parent exists, unless you pass optional: true.

TrapThe database can't enforce a polymorphic key. A renamed parent needs its stored _type values updated.

Callback order on a new record

before_validationafter_validationbefore_savebefore_createINSERTaround_create wraps thisafter_createafter_saveafter_commitor after_rollback

Tipthrow :abort halts the chain. An update runs the same chain with update.

TrapEverything up to after_save runs in the transaction. Send email from after_create_commit.

Delegation and monkey patching

Forward calls in Railsdelegate :name, to: :user
Prefix the name, or allow a nil targetprefix: trueallow_nil: true
Forward calls in plain Rubyextend Forwardable

TipMonkey patching reopens a class to change it. A refinement applies only where you call using.

TrapA gem or a later Rails version can define the same method, and whichever loads last wins.

Queries & eager loading

fetch the right rows in the fewest queries

find, find_by and where

Load by primary key, raise if missingUser.find(1)
Load the first match, nil if noneUser.find_by(email: e)
Build a chainable set of matchesUser.where(admin: true)

Trapwhere returns an empty relation when nothing matches, never nil. find_by! raises.

includes, preload, eager_load, joins

Load in separate queries, which can't filterpreload(:posts)
Load with a LEFT OUTER JOIN, to filtereager_load(:posts)
Let Rails pick, based on your conditionsincludes(:posts)
Filter with INNER JOIN, with no eager loadjoins(:posts).distinct

TipFor a SQL string condition, references(:posts) forces the join.

Trapjoins loads only the users. Reading user.posts afterwards still runs one query per user.

Speed up queries

Read a slow query's planrelation.explain
Process a large table in batchesUser.find_each
Read a count from a columncounter_cache: true

TrapEach index slows writes to its table and takes storage, so add only the ones your queries use.

Migrations, locks & scaling

change the schema and share the database

Migrations

Run the new ones, or revert the lastbin/rails db:migratebin/rails db:rollback

Tipdb/schema.rb is the snapshot that builds a fresh database.

Trapchange reverses itself only for reversible commands. Raw SQL needs up and down methods.

Migrate without downtime

Index without blocking writes (PostgreSQL)algorithm: :concurrently
Stop reading a column, drop it laterself.ignored_columns
Rename a columnAdd a new one, write to both, backfill. Move reads over, stop old writes, then drop it.

TipThe strong_migrations gem catches unsafe steps in development.

TrapSet a short lock timeout. A blocked migration then fails fast instead of making every query wait behind it.

Connection pool

Set the pool size per process, and the waitpool: 5checkout_timeout: 5

TrapTotal connections are the pool size times the process count, and must stay under the database's own limit.

Sharding and multi-tenancy

Decide when to shardWhen one primary can't handle the writes or the data size. Read replicas can't fix that.
Switch to a shardconnected_to(shard: :one)
Isolate tenants by row, schema or databaseRow-level is simplest, but one missed filter leaks data. The others isolate more and cost more.

TrapShards rule out joins and transactions across them. Rails doesn't choose the shard for you.

Security & sign-in

what Rails blocks and what you still add

Security headers and habits

Force HTTPS and send HSTSconfig.force_ssl = true
Add a Content Security PolicyUse the Ruby DSL in an initializer.
Audit your code (7.2+)bin/brakeman

Tipconfig.action_dispatch.default_headers holds X-Frame-Options and the other defaults.

TrapNever call html_safe or raw on user data. Those two turn off the escaping views do for you.

Rate limiting

Limit a controller, answering 429 (7.2+)rate_limit to: 10, within: 3.minutes
Reject before the routerrack-attack

TrapKeep the counters in a store every server shares. Otherwise the real limit multiplies.

JSON APIs

build, version and secure an API

API versioning

Put the version in the path, the simplest way/api/v1/users
Read it from a header, with a route constraintAccept

TrapAdditive changes ship in the current version. Only a breaking change, like a removed field, needs a new one.

API authentication

Secure server-to-server callsUse API keys, and store only a hash of each.
Check a token with no database lookupJWT.decode

TrapA JWT stays valid until it expires. Keep its lifetime short, and pair it with a refresh token.

Jobs, caching & WebSockets

do work later, reuse results, push updates

Background jobs and retries

Run slow work outside the requestMyJob.perform_later(id)
Keep jobs in the database (8.0+):solid_queue
Keep jobs in Redis:sidekiq
Retry a transient errorretry_on NetworkError
Drop a job a retry can't fixdiscard_on ActiveRecord::RecordNotFound

Tipretry_on defaults to a 3 second wait and 5 attempts. Make a job idempotent, meaning safe to run twice.

TrapThe result isn't ready when the response goes out, so show a pending state or notify the user later.

Cache stores and levels

Keep a large cache in the database (8.0+):solid_cache_store
Share entries in memory across servers:redis_cache_store:mem_cache_store
Cache part of a viewcache @product do
Cache any computed valueRails.cache.fetch(key)

TipA fragment goes stale when its record's updated_at changes. touch: true on belongs_to gives Russian doll caching.

Trap:memory_store gives each process its own copy, so several servers need a shared store.

Action Cable

Subscribe a channel, then send to allstream_from "chat_1"ActionCable.server.broadcast("chat_1", data)
Relay through the database (8.0+)solid_cable

TrapThe async adapter is for development and tests only, not for several app servers.

Hotwire, assets & uploads

the page, its files and its languages

Turbo and Stimulus

Replace one region of the pageturbo_frame_tag
Send a small change by element IDturbo_stream.append
Wire a DOM event to JavaScriptdata-action

TipTurbo Drive swaps in the new page's <body> on links and forms, and the server keeps rendering HTML.

Assets and JavaScript

Recall the older toolsSprockets compiled assets. Webpacker bundled JavaScript in Rails 6.
Load JavaScript with no build step, from Rails 7pin "application"
Only fingerprint and serve files, from Rails 8Propshaft

TipA fingerprint puts a digest of the content in the file name, so browsers can cache the file for a long time.

TrapImport maps can't use JSX or TypeScript, which need transpiling. Use jsbundling-rails for those.

File uploads

Attach a file to a modelhas_one_attached :avatar
Send it straight to the cloud servicedirect_upload: true

TrapActive Storage doesn't limit file type or size, so validate both before you accept an upload.

Test, debug & deploy

prove it works, find what broke, ship it

Stubs, mocks and API tests

Fix what a method returnsallow(user).to receive(:admin?).and_return(true)
Fail unless a method is calledexpect(mailer).to receive(:send_email)
Send JSON, then check the statusget users_url, as: :jsonassert_response :created

TrapA test that checks every call is brittle. It breaks when the internals change, even when the behavior is still correct.

Deploy

Precompile assets in the buildbin/rails assets:precompile
Deploy a Docker image to your servers (8.0+)kamal deploy

TipRun job workers next to the web processes, and monitor errors and queues.

TrapRun migrations before the new code takes traffic, and keep the old code working with the new schema.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 70 questions it links, not just the 9 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.