<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hassan Farooq</title>
    <description>The latest articles on DEV Community by Hassan Farooq (@hasan-dev).</description>
    <link>https://dev.to/hasan-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3075821%2F33a1af44-6027-4129-942f-ad1b0501bbd8.png</url>
      <title>DEV Community: Hassan Farooq</title>
      <link>https://dev.to/hasan-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hasan-dev"/>
    <language>en</language>
    <item>
      <title>Pagination, filtering, and sorting in a Rails JSON API</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 19:06:30 +0000</pubDate>
      <link>https://dev.to/hasan-dev/pagination-filtering-and-sorting-in-a-rails-json-api-22p3</link>
      <guid>https://dev.to/hasan-dev/pagination-filtering-and-sorting-in-a-rails-json-api-22p3</guid>
      <description>&lt;p&gt;Every list endpoint starts innocent. &lt;code&gt;GET /api/users&lt;/code&gt;, return them all, ship it. Then a customer signs up forty thousand users, someone asks to filter by role, and the front end wants the newest first. Now you need pagination, filtering, and sorting, and the way you wire them up decides whether the endpoint stays fast and safe or becomes a security hole that also happens to be slow. Here's how I build it, and where the Ransack gem helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The request shape
&lt;/h2&gt;

&lt;p&gt;I accept explicit, named params instead of letting the client send anything that smells like SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/users?role=admin&amp;amp;created_after=2026-01-01&amp;amp;sort=-created_at&amp;amp;page=2&amp;amp;per_page=25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A handy convention for sort: a leading minus means descending, so &lt;code&gt;sort=-created_at&lt;/code&gt; is newest first and &lt;code&gt;sort=created_at&lt;/code&gt; is oldest first. &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;per_page&lt;/code&gt; handle paging, with &lt;code&gt;per_page&lt;/code&gt; capped on the server so nobody asks for a million rows at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hand-rolled version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Api::UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Api&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
  &lt;span class="no"&gt;MAX_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="no"&gt;SORTABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[created_at name email]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"created_at &amp;gt;= ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_after&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_after&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort_clause&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;per_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:per_page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;MAX_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;
    &lt;span class="n"&gt;paged&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="n"&gt;paged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UserSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;meta: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;current_page: &lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;per_page: &lt;/span&gt;&lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;total_count: &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;total_pages: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_clause&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sort&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;SORTABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sort&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="ss"&gt;:desc&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:asc&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads as a lot, but it's doing four jobs: filter, sort, page, and report back where you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things that actually matter
&lt;/h2&gt;

&lt;p&gt;Whitelist the columns people can sort and filter by. This is the big one. If you drop &lt;code&gt;params[:sort]&lt;/code&gt; straight into &lt;code&gt;order(...)&lt;/code&gt;, you've handed the client an SQL injection vector and let them sort by some unindexed column that drags the database to its knees. So I keep a &lt;code&gt;SORTABLE&lt;/code&gt; allowlist and fall back to a sane default when the field isn't on it. Same thinking for filters: only known params get applied, everything else is ignored.&lt;/p&gt;

&lt;p&gt;Cap &lt;code&gt;per_page&lt;/code&gt;. Without a ceiling, &lt;code&gt;per_page=1000000&lt;/code&gt; either runs your app out of memory or makes Postgres sweat. Clamp it server side and move on.&lt;/p&gt;

&lt;p&gt;Index what you filter and sort by. Here that's &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;created_at&lt;/code&gt;. Without indexes, every page is a sequential scan, and sorting by an unindexed column means a full sort on every single request. Pagination doesn't save you from a missing index, it just hides the cost until the table grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metadata
&lt;/h2&gt;

&lt;p&gt;The response splits into &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;meta&lt;/code&gt;. &lt;code&gt;data&lt;/code&gt; is the page of records. &lt;code&gt;meta&lt;/code&gt; tells the client where it is: current page, per page, total count, total pages. The front end needs that to draw "page 3 of 47" and to know when to stop. On large endpoints I sometimes drop &lt;code&gt;total_count&lt;/code&gt;, because counting every matching row on each request gets expensive, and I return a &lt;code&gt;next_cursor&lt;/code&gt; instead. Which brings up the real decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offset versus cursor
&lt;/h2&gt;

&lt;p&gt;Offset pagination is what &lt;code&gt;LIMIT&lt;/code&gt; and &lt;code&gt;OFFSET&lt;/code&gt; give you, and what most gems do by default. It's simple and it lets you jump straight to any page. Two problems show up at scale. &lt;code&gt;OFFSET 1000000&lt;/code&gt; still makes Postgres walk and throw away a million rows before it returns yours, so deep pages crawl. And if rows get inserted or deleted while someone is paging, the window shifts under them and they see the same record twice or miss one entirely.&lt;/p&gt;

&lt;p&gt;Cursor pagination, also called keyset pagination, uses a stable pointer instead of a row count. You ask for &lt;code&gt;WHERE created_at &amp;lt; ? ORDER BY created_at DESC LIMIT 25&lt;/code&gt; and hand back the last value you saw as the next cursor. It stays fast at any depth because it seeks straight into the index instead of counting past rows, and it's stable when data changes underneath. The tradeoff is you only get next and previous, not "jump to page 47." I use offset for admin tables and small lists, and cursor for big feeds and exports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Ransack comes in
&lt;/h2&gt;

&lt;p&gt;For paging I reach for Pagy, which is tiny and fast, or Kaminari if I want the more featureful one. For filtering and sorting, Ransack is the usual pick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile: gem "ransack"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ransack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;      &lt;span class="c1"&gt;# ?q[role_eq]=admin&amp;amp;q[created_at_gteq]=2026-01-01&lt;/span&gt;
  &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sort&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"created_at desc"&lt;/span&gt;
  &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:page&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;per&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# with Kaminari&lt;/span&gt;
  &lt;span class="c1"&gt;# render data + meta&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a whole query language for free. &lt;code&gt;role_eq&lt;/code&gt; for equality, &lt;code&gt;name_cont&lt;/code&gt; for "contains", &lt;code&gt;created_at_gteq&lt;/code&gt; for a date floor, sorting through &lt;code&gt;q.sorts&lt;/code&gt;, even filtering across associations. For an admin screen with a dozen filter combinations, it saves a real amount of code.&lt;/p&gt;

&lt;p&gt;There's a catch worth saying out loud, because it bit a lot of people. By default Ransack exposes your whole schema to the client. Every column becomes queryable, which is both a data leak and a way to run slow queries against unindexed columns. Modern Ransack makes you opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ransackable_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="sx"&gt;%w[role created_at name email]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ransackable_associations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the exact same whitelist idea from the hand-rolled version, just expressed through Ransack's hooks. For a small public API I usually prefer explicit params, because the attack surface is smaller and I control every query. For a big internal admin, Ransack earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Accept explicit params, whitelist the columns you sort and filter on, cap &lt;code&gt;per_page&lt;/code&gt;, and index whatever you filter or sort by. Return &lt;code&gt;data&lt;/code&gt; plus &lt;code&gt;meta&lt;/code&gt;. Use offset pagination for small and admin lists, cursor pagination for large feeds. Reach for Pagy or Kaminari to page and Ransack to filter, and whatever you do, lock Ransack down with &lt;code&gt;ransackable_attributes&lt;/code&gt; so it isn't quietly exposing your whole database.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>api</category>
      <category>rest</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building a production-ready create endpoint in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:56:38 +0000</pubDate>
      <link>https://dev.to/hasan-dev/building-a-production-ready-create-endpoint-in-rails-4dko</link>
      <guid>https://dev.to/hasan-dev/building-a-production-ready-create-endpoint-in-rails-4dko</guid>
      <description>&lt;p&gt;"Design an endpoint to create an order" sounds like a five-line answer. Route, controller, save, done. The reason interviewers like it is that the five-line version skips everything that actually matters in production: the right status codes, where the slow work goes, and what happens when the same request arrives twice. Here's how I'd build it and explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The route
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /orders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;POST because creating a resource is neither safe nor idempotent. The path is the plural resource name, and creating against the collection is the REST convention for "make a new one." That part really is the easy bit, so don't spend your interview minutes there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the controller does, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;The controller handles the HTTP layer and nothing else. It authenticates, authorizes, permits params, hands the actual work to a service, and turns the result into a response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_user!&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OrderCreator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;order_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="no"&gt;OrderSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;order_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's not in there. No Stripe call, no mailer, no transaction, no line-item math. Those belong in &lt;code&gt;OrderCreator&lt;/code&gt;. If you ever want proof that a controller is too fat, count how many reasons it would have to change. This one changes only when the HTTP contract changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status codes are part of the design
&lt;/h2&gt;

&lt;p&gt;This is where the junior answer usually returns &lt;code&gt;200&lt;/code&gt; for everything and moves on. The status code is how the client knows what happened without parsing your body, so it's worth getting right.&lt;/p&gt;

&lt;p&gt;Return &lt;code&gt;201 Created&lt;/code&gt; on success, not &lt;code&gt;200&lt;/code&gt;, and include the new order in the body. A &lt;code&gt;Location&lt;/code&gt; header pointing at the new resource is a nice touch.&lt;/p&gt;

&lt;p&gt;Return &lt;code&gt;422 Unprocessable Entity&lt;/code&gt; when validation fails, with a structured error body so the front end can show messages next to the right fields.&lt;/p&gt;

&lt;p&gt;Return &lt;code&gt;401 Unauthorized&lt;/code&gt; when there's no valid auth, and &lt;code&gt;403 Forbidden&lt;/code&gt; when the user is logged in but not allowed to do this. Those two get conflated constantly, and keeping them straight signals you've actually built APIs.&lt;/p&gt;

&lt;p&gt;Return &lt;code&gt;400 Bad Request&lt;/code&gt; when the input is malformed, like the whole &lt;code&gt;order&lt;/code&gt; key is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the real work goes
&lt;/h2&gt;

&lt;p&gt;The service creates the order and its line items inside a transaction, so they commit together or not at all. The slow and external stuff, charging the card, sending the confirmation email, syncing to a CRM, goes to background jobs. A user shouldn't wait on an SMTP server, and a third party having a bad day shouldn't take your order endpoint down with it. I covered the transaction-versus-external-call reasoning in more depth in my MVC post, but the short version is that a charge can't be rolled back, so it lives outside the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part almost everyone forgets: duplicate requests
&lt;/h2&gt;

&lt;p&gt;A user double-taps the buy button. Or the network hiccups and the client retries. Either way the same POST hits you twice, and a naive endpoint cheerfully creates two identical orders and charges the card twice.&lt;/p&gt;

&lt;p&gt;The fix is an idempotency key. The client generates a unique key per logical request and sends it in a header. You store that key with a unique constraint, and if a second request shows up with the same key, you return the original result instead of creating anything new.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;

  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OrderCreator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;order_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...store the key with the result, then render&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly how Stripe's own API handles retries, which is a good thing to mention because it shows you've read how a serious payments API solves the same problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding it out
&lt;/h2&gt;

&lt;p&gt;A few things I'd name to show I'm thinking past one endpoint. A consistent JSON error shape across the whole API so clients parse errors one way. Pagination and filtering on the list endpoint, &lt;code&gt;GET /orders&lt;/code&gt;, because it will get slow once a customer has thousands. And versioning under &lt;code&gt;/api/v1/&lt;/code&gt; so a future breaking change doesn't break every client at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /orders&lt;/code&gt; with a thin controller that authenticates, permits params, and calls a service. &lt;code&gt;201&lt;/code&gt; on success, &lt;code&gt;422&lt;/code&gt; on validation errors, and the right &lt;code&gt;401&lt;/code&gt; versus &lt;code&gt;403&lt;/code&gt; for auth. Real work in a transaction, slow work in background jobs, and an idempotency key so a double-tap or a retry doesn't create a second order. The route was never the hard part. Everything after it is.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>api</category>
      <category>restapi</category>
      <category>backend</category>
    </item>
    <item>
      <title>validates vs validate in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:36:55 +0000</pubDate>
      <link>https://dev.to/hasan-dev/validates-vs-validate-in-rails-5bbp</link>
      <guid>https://dev.to/hasan-dev/validates-vs-validate-in-rails-5bbp</guid>
      <description>&lt;p&gt;The difference between &lt;code&gt;validates&lt;/code&gt; and &lt;code&gt;validate&lt;/code&gt; in Rails is one letter, and that letter changes everything. One is the built-in helper you feed a list of rules. The other is a hook for a method you write yourself. People mix them up because they read almost the same out loud, so the way I keep them straight is to remember that the plural one comes with rules included and the singular one is bring-your-own.&lt;/p&gt;

&lt;h2&gt;
  
  
  validates: the built-in rules
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;validates&lt;/code&gt;, with the s, takes an attribute and a set of standard rules that Rails already knows how to enforce. Presence, uniqueness, length, numericality, format, and so on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;uniqueness: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;numericality: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;greater_than: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;length: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;maximum: &lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're not writing any logic here. You're declaring what should be true and letting Rails supply the checking. This covers the large majority of everyday validations, and if a built-in rule fits, use it instead of hand-rolling anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  validate: your own method
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;validate&lt;/code&gt;, no s, registers a method you wrote. You reach for it when the rule is specific to your domain or depends on more than one field, so no built-in helper covers it. The classic example is making sure an event doesn't end before it starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="ss"&gt;:end_after_start&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_after_start&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;starts_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ends_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ends_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;starts_at&lt;/span&gt;
      &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ends_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"must be after the start time"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in that method matter, and they're the parts people get wrong the first time.&lt;/p&gt;

&lt;p&gt;The guard clause comes first. I return early if either timestamp is missing, because whether the fields are present is a different question. That's a job for &lt;code&gt;validates :starts_at, :ends_at, presence: true&lt;/code&gt;. My custom method only cares about the order of two values that already exist. Skip the guard and you get a nil comparison blowing up, plus a confusing second error on top of the presence one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;errors.add&lt;/code&gt; is how a custom validation fails. You attach a message to a specific attribute, and that's what flips &lt;code&gt;valid?&lt;/code&gt; to false and what shows up in &lt;code&gt;event.errors&lt;/code&gt; for the form to display. No &lt;code&gt;errors.add&lt;/code&gt;, no failure, the record saves happily.&lt;/p&gt;

&lt;p&gt;The method is private. Nothing outside the model should be calling &lt;code&gt;end_after_start&lt;/code&gt; directly, so it lives below a &lt;code&gt;private&lt;/code&gt; line with the rest of the internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both of these stop at the application layer
&lt;/h2&gt;

&lt;p&gt;Here's the part worth saying out loud in an interview. &lt;code&gt;validates&lt;/code&gt; and &lt;code&gt;validate&lt;/code&gt; both run in Ruby, before the INSERT or UPDATE. They're perfect for friendly error messages on a form. They do not protect you under concurrency.&lt;/p&gt;

&lt;p&gt;Picture two requests signing up with the same email at the same instant. Both run the uniqueness check, both see no existing row, both pass, both insert. Now you have duplicate emails and a validation that swore it prevented exactly that. The fix is a database constraint underneath the validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is validation for the user experience, constraint for correctness. For the event example you can go a step further and add a Postgres check so the rule holds even if a row gets written outside the Rails model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_check_constraint&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ends_at &amp;gt; starts_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"events_end_after_start"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;validates&lt;/code&gt; with an s is for Rails' built-in rules you declare. &lt;code&gt;validate&lt;/code&gt; with no s is for a custom method you write, where the guard clause and &lt;code&gt;errors.add&lt;/code&gt; do the work. And neither one is a substitute for a database constraint when the data absolutely has to be correct. Use the validation so the user gets a clear message, and back it with a constraint so a race condition can't slip past.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>validations</category>
      <category>activerecord</category>
    </item>
    <item>
      <title>Scopes vs class methods in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:25:29 +0000</pubDate>
      <link>https://dev.to/hasan-dev/scopes-vs-class-methods-in-rails-4emf</link>
      <guid>https://dev.to/hasan-dev/scopes-vs-class-methods-in-rails-4emf</guid>
      <description>&lt;p&gt;Scopes and class methods in Rails do almost the same job, which is exactly why the question trips people up. The honest answer is that a scope is basically a class method that returns a query, with one safety feature bolted on. Once you know what that feature is, you know when to use which. The whole thing comes down to a single &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a scope actually is
&lt;/h2&gt;

&lt;p&gt;A scope is a named, reusable piece of a query. You define it with &lt;code&gt;scope&lt;/code&gt;, hand it a lambda, and it gives you back an &lt;code&gt;ActiveRecord::Relation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;published: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:recent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:by_author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author_id: &lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things make that useful. Because every scope returns a relation, you can chain them, and because a relation is lazy, no SQL runs until you actually read the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;recent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each call hands the next one a relation to build on, so all of that collapses into a single SELECT at the end. You're composing one query out of small named parts, not running three.&lt;/p&gt;

&lt;h2&gt;
  
  
  They're closer than you think
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprises people. A scope and a class method that returns a relation are nearly identical. These two are equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;published: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published&lt;/span&gt;
  &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;published: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both give you &lt;code&gt;Post.published&lt;/code&gt;. Both return a relation. Both chain. So if they're the same, why have both? The difference only shows up when the logic returns nothing useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nil that breaks the chain
&lt;/h2&gt;

&lt;p&gt;A scope has one guarantee a plain method doesn't. If the body of a scope returns &lt;code&gt;nil&lt;/code&gt;, Rails quietly swaps in &lt;code&gt;all&lt;/code&gt;, so the chain keeps working. A class method has no such net. Return &lt;code&gt;nil&lt;/code&gt; from it and the next method in the chain blows up.&lt;/p&gt;

&lt;p&gt;Picture a search with a guard clause for a blank term:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

  &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"title ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sanitize_sql_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;return all&lt;/code&gt; is the whole point. If you wrote &lt;code&gt;return nil&lt;/code&gt; instead, then &lt;code&gt;Post.search("").recent&lt;/code&gt; would call &lt;code&gt;.recent&lt;/code&gt; on &lt;code&gt;nil&lt;/code&gt; and you'd get a NoMethodError. The scope version would have rescued you by falling back to &lt;code&gt;all&lt;/code&gt; automatically, but inside a class method you're responsible for returning a relation yourself.&lt;/p&gt;

&lt;p&gt;So the rule I follow:&lt;/p&gt;

&lt;p&gt;Use a scope for simple, always-chainable fragments, especially one-liners like the three on the Post model above.&lt;/p&gt;

&lt;p&gt;Use a class method when there's real logic, a conditional, a guard clause, or several steps, because then you want to control the relation by hand and return &lt;code&gt;all&lt;/code&gt; on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing scopes are not for
&lt;/h2&gt;

&lt;p&gt;Scopes are for shaping queries, not for doing work. A scope should be a &lt;code&gt;where&lt;/code&gt;, an &lt;code&gt;order&lt;/code&gt;, a &lt;code&gt;joins&lt;/code&gt;, something that narrows or sorts records. The moment you catch a scope sending an email, updating rows, or calling an API, it's in the wrong place. That isn't query composition anymore, and it belongs in a service object.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Scopes are named, chainable queries that always hand back a relation. A class method does the same job but gives you room for branching and conditionals. Reach for a scope when it's a clean one-liner, reach for a class method when there's logic involved, and whichever you pick, return &lt;code&gt;all&lt;/code&gt; instead of &lt;code&gt;nil&lt;/code&gt; so the next link in the chain doesn't snap.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>ruby</category>
      <category>database</category>
    </item>
    <item>
      <title>How to add a NOT NULL column to a large table safely in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:04:28 +0000</pubDate>
      <link>https://dev.to/hasan-dev/how-to-add-a-not-null-column-to-a-large-table-safely-in-rails-41p9</link>
      <guid>https://dev.to/hasan-dev/how-to-add-a-not-null-column-to-a-large-table-safely-in-rails-41p9</guid>
      <description>&lt;p&gt;A migration that runs in two milliseconds on your laptop can lock a production table for thirty seconds and pile up every request behind it. The gap is data. Your dev table has twelve rows. The production table has twelve million. An interviewer asked me how I'd add a required status column to a large orders table, and the real answer is that you don't do it in one migration, you do it in four small ones.&lt;/p&gt;

&lt;p&gt;First, the part people skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a migration actually is
&lt;/h2&gt;

&lt;p&gt;A migration is a versioned schema change written as Ruby. Each one has a timestamp, Rails records which have run in a &lt;code&gt;schema_migrations&lt;/code&gt; table, and &lt;code&gt;db/schema.rb&lt;/code&gt; always reflects the current shape of the database. The win is that schema changes become reviewable code that every environment applies in the same order, instead of someone SSHing into production and running ALTER TABLE by hand at 11pm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddEmailToUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt;  &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That migration is totally fine on a small table. The trouble only shows up at scale, so that's where the interesting decisions live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when the table is big and live
&lt;/h2&gt;

&lt;p&gt;On your machine the migration runs against an empty table while nothing else is happening. In production it runs against millions of rows while real traffic hits the same table. Four things start to matter that never mattered in dev.&lt;/p&gt;

&lt;p&gt;Locks. Some operations grab a lock that blocks reads or writes while they run. Hold that lock on a busy table for a few seconds and requests stack up behind it until something times out.&lt;/p&gt;

&lt;p&gt;Table size. Anything that has to touch every row, like a backfill or a column rewrite, takes time proportional to row count. Twelve million rows is a different animal from twelve.&lt;/p&gt;

&lt;p&gt;Rolling deploys. During a deploy, old and new versions of your code run at the same time against the same database. The schema has to work for both versions at every moment. This is the part that catches people, and it's why a single migration often can't be safe.&lt;/p&gt;

&lt;p&gt;Reversibility. If a deploy goes wrong you want to roll back cleanly, so I want each step to be reversible on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;Here's the migration almost everyone writes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, reads great, and it's a landmine on a large table. Two problems. On older Postgres, adding a column with a default rewrites every single row while holding a lock, so the table is frozen for the length of that rewrite. And the &lt;code&gt;null: false&lt;/code&gt; breaks your rolling deploy the instant the old code, which knows nothing about status, inserts an order without it.&lt;/p&gt;

&lt;p&gt;The fix is to stop thinking in one migration and start thinking in phases, each one safe on its own and shipped separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: add the column, nullable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddStatusToOrders&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Postgres 11 and up, adding a column with a constant default is a metadata change. It does not rewrite the table, so it returns almost instantly. New rows get "pending". Old rows stay NULL for now, and that's fine, because the column is still nullable and old code can keep inserting without touching it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: backfill the old rows in batches
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BackfillOrderStatus&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;disable_ddl_transaction!&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;
    &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;in_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;of: &lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;down&lt;/span&gt;
    &lt;span class="c1"&gt;# nothing to undo&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point of batching is to never hold a long lock. Ten thousand rows at a time, each in its own quick transaction, with a short pause so other queries get a turn. Updating all twelve million in one statement would be one enormous transaction sitting on a lock, which is the thing we're trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: ship the code that sets status
&lt;/h2&gt;

&lt;p&gt;Deploy your application change so every path that creates an order sets status explicitly. After this point, nothing writes a NULL anymore. This is a code deploy, not a migration, and it has to land before the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: enforce NOT NULL safely
&lt;/h2&gt;

&lt;p&gt;Now that the column is fully populated and nobody writes NULL, you can add the constraint. Doing it the blunt way still scans the whole table under a lock to verify, so on Postgres I validate it in two moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MakeOrderStatusNotNull&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;
    &lt;span class="n"&gt;add_check_constraint&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status IS NOT NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"orders_status_not_null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;validate: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;validate_check_constraint&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"orders_status_not_null"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;down&lt;/span&gt;
    &lt;span class="n"&gt;remove_check_constraint&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"orders_status_not_null"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding the constraint as &lt;code&gt;validate: false&lt;/code&gt; is instant because it doesn't check existing rows yet. Then &lt;code&gt;validate_check_constraint&lt;/code&gt; checks them without taking the heavy lock that a plain NOT NULL would. Once it passes, you have the guarantee you wanted and nobody noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A migration is versioned schema-as-code, and the moment a table gets big and busy you care about locks, table size, rolling deploys, and clean rollbacks. To add a required column: add it nullable, backfill in batches, deploy code that sets it, then enforce NOT NULL. The one rule worth tattooing on your hand is never add a NOT NULL column with a backfill in a single migration on a large table.&lt;/p&gt;

&lt;p&gt;One tool that makes this automatic: the strong_migrations gem. It flags unsafe migrations in code review and prints the safe version for you, so you catch the landmine before it ships instead of during the incident.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>postgressql</category>
      <category>migrations</category>
      <category>database</category>
    </item>
    <item>
      <title>Stop putting side effects in Rails callbacks</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 15:27:16 +0000</pubDate>
      <link>https://dev.to/hasan-dev/stop-putting-side-effects-in-rails-callbacks-d9h</link>
      <guid>https://dev.to/hasan-dev/stop-putting-side-effects-in-rails-callbacks-d9h</guid>
      <description>&lt;p&gt;Callbacks are one of those Rails features that feel great for about three months and then quietly ruin your afternoon. The question I got asked in an interview was simple: are model callbacks good to use, and how do you avoid overusing them? My answer is that they're good for data and bad for behavior, and the trouble starts the moment you forget which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where callbacks earn their keep
&lt;/h2&gt;

&lt;p&gt;The good use is small, record-local cleanup. Stuff that's only about keeping the row itself tidy, and that should happen no matter who saves the record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;before_validation&lt;/span&gt; &lt;span class="ss"&gt;:normalize_email&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_email&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downcasing an email, stripping whitespace, setting a default, generating a slug from a title. These are cheap, predictable, and they don't reach outside the record. Nobody gets surprised by a callback that lowercases an email. This is exactly what &lt;code&gt;before_validation&lt;/code&gt; and &lt;code&gt;before_save&lt;/code&gt; are for, and I'd reach for them without a second thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they turn on you
&lt;/h2&gt;

&lt;p&gt;Now the bad use. Side effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;after_create&lt;/span&gt; &lt;span class="ss"&gt;:charge_customer&lt;/span&gt;      &lt;span class="c1"&gt;# external API&lt;/span&gt;
  &lt;span class="n"&gt;after_create&lt;/span&gt; &lt;span class="ss"&gt;:send_confirmation&lt;/span&gt;    &lt;span class="c1"&gt;# email&lt;/span&gt;
  &lt;span class="n"&gt;after_create&lt;/span&gt; &lt;span class="ss"&gt;:sync_to_crm&lt;/span&gt;          &lt;span class="c1"&gt;# another external API&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks tidy. It isn't. A plain &lt;code&gt;Order.create&lt;/code&gt; now secretly charges a card, sends an email, and pokes a third party, and none of that is visible at the place you called &lt;code&gt;create&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it bites. Six months later someone writes a rake task to backfill old orders, calls &lt;code&gt;Order.create&lt;/code&gt;, and accidentally emails five thousand customers and runs five thousand charges. Or you sit down to write a test for something unrelated to payments, and you can't, because every &lt;code&gt;create&lt;/code&gt; in the suite now needs Stripe, the mailer, and the CRM stubbed. The save and the side effects are welded together, and you can't pull them apart.&lt;/p&gt;

&lt;p&gt;There's also a sharp edge most people meet by accident. &lt;code&gt;destroy_all&lt;/code&gt; runs callbacks, &lt;code&gt;delete_all&lt;/code&gt; skips them. Same intent, "remove these rows," two different behaviors depending on which method you typed. If your cleanup logic lives in a callback, one of those quietly does the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to keep them from spreading
&lt;/h2&gt;

&lt;p&gt;The trick is noticing when a callback stopped being cleanup and turned into a workflow. The fix is to pull that workflow out into a service object you call on purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCreator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;PaymentCharger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;OrderMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
    &lt;span class="no"&gt;CrmSyncJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;order&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same work, completely different feel. The side effects are right there in front of you, in order, on purpose. A test that just needs an order calls &lt;code&gt;create!&lt;/code&gt; and gets an order, nothing else. The jobs are queued because you asked, not because saving a row happened to trigger them. When the CRM sync breaks, you know exactly where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line I'd actually say
&lt;/h2&gt;

&lt;p&gt;Use callbacks for data, not for behavior. If a callback only touches the record's own attributes, leave it. The second it reaches outside the record, like sending mail, calling an API, or orchestrating other models, it belongs in a service object.&lt;/p&gt;

&lt;p&gt;One honest caveat so you don't sound like a zealot. &lt;code&gt;after_commit&lt;/code&gt; is a reasonable middle ground for "enqueue a job once this record is actually saved," because it only fires after the transaction commits, not on a rollback. Even then I usually enqueue from the service layer instead, just to keep the flow visible. Both are defensible. The thing worth being able to explain is &lt;em&gt;why&lt;/em&gt; fat callbacks hurt, because that's the part that tells an interviewer you've been burned by them before.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Fat controllers, fat models, and the layer MVC forgot</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 14:52:43 +0000</pubDate>
      <link>https://dev.to/hasan-dev/fat-controllers-fat-models-and-the-layer-mvc-forgot-1g9c</link>
      <guid>https://dev.to/hasan-dev/fat-controllers-fat-models-and-the-layer-mvc-forgot-1g9c</guid>
      <description>&lt;p&gt;MVC is the first thing anyone learns about Rails and the last thing people actually get right. The pattern itself is simple. Three layers, each with one job. Where it falls apart is the stuff that doesn't fit cleanly into any of the three, and that's exactly what interviewers poke at.&lt;/p&gt;

&lt;p&gt;I'll walk through the three layers fast, then spend the real time on the question that separates a junior answer from a mid-level one: where do you put the logic for creating an order, charging Stripe, and sending a confirmation email?&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers
&lt;/h2&gt;

&lt;p&gt;The model owns your data and the rules attached to it. With Active Record that means associations, validations, scopes, and small bits of behavior that belong to the record itself, like &lt;code&gt;order.total&lt;/code&gt; or &lt;code&gt;user.full_name&lt;/code&gt;. If the question is "what is true about this record," the answer lives in the model.&lt;/p&gt;

&lt;p&gt;The controller handles one request. It reads params, checks who you are and what you're allowed to do, calls a model or a service, and picks a response. That's it. A controller should read like a list of instructions a manager gives, not like the work itself. People call a bloated one a "fat controller," and it's a smell.&lt;/p&gt;

&lt;p&gt;The view renders what the user sees. An ERB template, a Turbo Stream, a JSON response from a serializer. Presentation only. The view should never decide business rules. It just shows whatever the controller handed it.&lt;/p&gt;

&lt;p&gt;A request walks through them in order. The router matches the URL to a controller action, the controller calls a model or service, the result goes to a view, and the response heads back to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody teaches
&lt;/h2&gt;

&lt;p&gt;Here's the trap. Rails makes it so easy to put code in the model or the controller that people put everything there. Both rot the same way.&lt;/p&gt;

&lt;p&gt;Stuff everything in the model and you get a 600-line &lt;code&gt;User&lt;/code&gt; class that sends emails, talks to Stripe, and syncs a CRM, all triggered by callbacks you forgot existed. Stuff everything in the controller and you get a &lt;code&gt;create&lt;/code&gt; action that's forty lines of business logic with rendering buried at the bottom.&lt;/p&gt;

&lt;p&gt;The fix is to notice when an operation isn't really model data and isn't really request handling. When something coordinates several models, calls an external service, or kicks off side effects, it's a workflow. Workflows go in a service object. That's the missing letter MVC never gave you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order example
&lt;/h2&gt;

&lt;p&gt;So you need to create an order, charge the customer through Stripe, and email them a confirmation. Three things, and they each belong in a different place.&lt;/p&gt;

&lt;p&gt;Start with the controller. It should stay boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OrderCreator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;order_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;order_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing. No Stripe, no mailer, no transaction. The controller checks the user (through &lt;code&gt;current_user&lt;/code&gt;), permits params, hands off to a service, and turns the result into a response. If you came back in a year and the only change was the JSON shape, this is the only file you'd touch.&lt;/p&gt;

&lt;p&gt;Now the service, where the actual workflow lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCreator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="vi"&gt;@params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

    &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_line_items_from_cart!&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;OrderMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;

    &lt;span class="no"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="no"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"payment_failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;currency: &lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stripe_customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idempotency_key: &lt;/span&gt;&lt;span class="s2"&gt;"order-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three pieces, three homes:&lt;/p&gt;

&lt;p&gt;The order and its line items are database writes that have to succeed or fail together. You don't want an order with no line items, or line items pointing at an order that never saved. So they go inside a transaction. I use &lt;code&gt;create!&lt;/code&gt; with the bang so a failure raises and the transaction rolls back instead of silently saving half the data. The order's own rules, like &lt;code&gt;validates :quantity, numericality: { greater_than: 0 }&lt;/code&gt;, still live on the &lt;code&gt;Order&lt;/code&gt; model where they belong.&lt;/p&gt;

&lt;p&gt;The Stripe charge sits in the service but outside the transaction, and that placement is deliberate. A transaction holds a database connection and often locks rows. If you wrap a network call to Stripe inside it, you're holding that lock for as long as Stripe takes to answer, which could be seconds. Worse, a charge can't be rolled back. If the DB transaction fails after the card was charged, your database and Stripe now disagree and the customer is out real money. So I charge after the transaction commits, with an idempotency key so a retry never double-charges.&lt;/p&gt;

&lt;p&gt;The email goes to a background job with &lt;code&gt;deliver_later&lt;/code&gt;. The customer shouldn't sit and wait for an SMTP server, and a flaky mail provider shouldn't blow up an otherwise good order. Note I pass &lt;code&gt;order.id&lt;/code&gt;, not the order object, because the job runs later and should reload fresh data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner
&lt;/h2&gt;

&lt;p&gt;If an interviewer asks and you've got ten seconds: controllers coordinate the request, models hold data and record-level rules, views render, and any multi-step workflow with side effects goes in a service object. The order example is the proof. Creation in a transaction, the external charge outside it, the email in a job, and a controller thin enough to read in one breath.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>mvc</category>
      <category>architecture</category>
    </item>
    <item>
      <title>HABTM, has_many through, STI, and polymorphic associations in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:31:08 +0000</pubDate>
      <link>https://dev.to/hasan-dev/habtm-hasmany-through-sti-and-polymorphic-associations-in-rails-5dlp</link>
      <guid>https://dev.to/hasan-dev/habtm-hasmany-through-sti-and-polymorphic-associations-in-rails-5dlp</guid>
      <description>&lt;p&gt;Once you're past &lt;code&gt;belongs_to&lt;/code&gt; and &lt;code&gt;has_many&lt;/code&gt;, Rails gives you four more association tools that confuse a lot of people in interviews: &lt;code&gt;has_and_belongs_to_many&lt;/code&gt;, &lt;code&gt;has_many :through&lt;/code&gt;, single table inheritance, and polymorphic associations. They sound advanced, but each one solves a specific, concrete problem. The trick is knowing which problem.&lt;/p&gt;

&lt;p&gt;I'll go through all four with real tables and real code, then end on the one comparison interviewers love to ask: why pick &lt;code&gt;has_many :through&lt;/code&gt; over HABTM.&lt;/p&gt;

&lt;h2&gt;
  
  
  has_and_belongs_to_many (HABTM)
&lt;/h2&gt;

&lt;p&gt;This is the simplest way to model a many-to-many relationship. Say a user can be on many projects, and a project can have many users. You set it up on both sides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_and_belongs_to_many&lt;/span&gt; &lt;span class="ss"&gt;:projects&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_and_belongs_to_many&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes there's a join table that holds nothing but the two foreign keys. By convention Rails wants it named after both tables in alphabetical order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:projects_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:project&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;id: false&lt;/code&gt;. The join table has no primary key and, more importantly, no model. You never write &lt;code&gt;class ProjectsUser&lt;/code&gt;. That's the whole point of HABTM: it's lightweight. You get &lt;code&gt;user.projects&lt;/code&gt; and &lt;code&gt;project.users&lt;/code&gt; and that's it.&lt;/p&gt;

&lt;p&gt;It's also where HABTM runs out of room. There's no model, so there's nowhere to put a &lt;code&gt;role&lt;/code&gt;, a &lt;code&gt;joined_at&lt;/code&gt; timestamp, a validation, or a callback. The join is just a link and stays a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  has_many :through
&lt;/h2&gt;

&lt;p&gt;This is the grown-up version of the same many-to-many. Instead of a bare join table, you create a real model for the relationship and route through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:memberships&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;through: :memberships&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:memberships&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;through: :memberships&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Membership&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:project&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration looks like a normal table because it is one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:memberships&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"member"&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="ss"&gt;:joined_at&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the relationship can carry data. A membership has a role, a joined date, timestamps, validations, whatever you need. You can ask &lt;code&gt;user.memberships.where(role: "admin")&lt;/code&gt; or add &lt;code&gt;validates :role, inclusion: { in: %w[member admin owner] }&lt;/code&gt; on the &lt;code&gt;Membership&lt;/code&gt; model. None of that is possible with a plain HABTM join.&lt;/p&gt;

&lt;p&gt;You still get the convenient shortcut: &lt;code&gt;user.projects&lt;/code&gt; skips straight past memberships to the projects on the other side. You just also have the join model available when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single table inheritance (STI)
&lt;/h2&gt;

&lt;p&gt;STI is for when several models are variations on a theme and share most of their columns. Instead of one table per model, they all live in one table, and Rails uses a &lt;code&gt;type&lt;/code&gt; column to remember which class each row belongs to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no separate &lt;code&gt;admins&lt;/code&gt; or &lt;code&gt;customers&lt;/code&gt; table. Everything sits in &lt;code&gt;users&lt;/code&gt;, and you just need that &lt;code&gt;type&lt;/code&gt; column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;Admin.create(email: "a@example.com")&lt;/code&gt;, Rails writes a row to &lt;code&gt;users&lt;/code&gt; with &lt;code&gt;type&lt;/code&gt; set to &lt;code&gt;"Admin"&lt;/code&gt;. When you later run &lt;code&gt;Admin.all&lt;/code&gt;, it automatically adds &lt;code&gt;WHERE type = 'Admin'&lt;/code&gt; for you. &lt;code&gt;User.all&lt;/code&gt; still returns everyone, and each record comes back as the right subclass, so an admin row gives you back an &lt;code&gt;Admin&lt;/code&gt; instance with all its methods.&lt;/p&gt;

&lt;p&gt;STI is great when the subclasses are genuinely alike and differ mostly in behavior. It gets ugly fast when they don't. If &lt;code&gt;Admin&lt;/code&gt; needs five columns that &lt;code&gt;Customer&lt;/code&gt; never uses, every customer row carries five NULLs forever, and the table turns into a junk drawer. The rule I use: STI when subclasses share almost all their columns, separate tables when they diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polymorphic associations
&lt;/h2&gt;

&lt;p&gt;A polymorphic association lets one model belong to more than one kind of parent, without a separate foreign key for each one. The classic example is comments. You want to comment on a post, and also on a photo, and maybe later on a video. You don't want &lt;code&gt;post_id&lt;/code&gt;, &lt;code&gt;photo_id&lt;/code&gt;, and &lt;code&gt;video_id&lt;/code&gt; columns sitting mostly empty on the comments table.&lt;/p&gt;

&lt;p&gt;Instead you store two columns: one for the parent's id, and one for the parent's type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:commentable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :commentable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Photo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :commentable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration uses &lt;code&gt;references&lt;/code&gt; with &lt;code&gt;polymorphic: true&lt;/code&gt;, which creates both columns at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:commentable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you &lt;code&gt;commentable_type&lt;/code&gt; (a string like &lt;code&gt;"Post"&lt;/code&gt; or &lt;code&gt;"Photo"&lt;/code&gt;) and &lt;code&gt;commentable_id&lt;/code&gt;. When you save a comment on a post, Rails stores &lt;code&gt;"Post"&lt;/code&gt; and the post's id. When you read &lt;code&gt;comment.commentable&lt;/code&gt;, it uses the type to know which table to look in. You can comment on any model that declares &lt;code&gt;has_many :comments, as: :commentable&lt;/code&gt;, and adding a new commentable type later means zero schema changes.&lt;/p&gt;

&lt;p&gt;One thing to watch: a polymorphic column can't have a normal database foreign key constraint, because the database doesn't know which table the id points to. You give up that bit of referential integrity in exchange for the flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when do you pick has_many :through over HABTM?
&lt;/h2&gt;

&lt;p&gt;This is the question that comes up most, and the answer is short: the moment the relationship itself needs to hold anything.&lt;/p&gt;

&lt;p&gt;HABTM gives you a link and nothing else. The second you need a role on the membership, or a timestamp for when someone joined, or a validation that stops duplicates, or a callback, or even just the ability to query the join directly, HABTM has no place to put it. There's no model. You'd have to migrate to &lt;code&gt;has_many :through&lt;/code&gt; anyway, and migrating later is more painful than starting there.&lt;/p&gt;

&lt;p&gt;So in practice I almost always reach for &lt;code&gt;has_many :through&lt;/code&gt;. HABTM is fine for a pure tag-style link that you're certain will never grow attributes, like connecting articles to categories where the connection is just a connection. But "never" is a strong word, and most join tables sprout a column eventually. Starting with a join model costs you one extra file today and saves you a migration and a refactor down the line.&lt;/p&gt;

&lt;p&gt;If you can only remember one line for the interview: use HABTM when the relationship is just a link, and &lt;code&gt;has_many :through&lt;/code&gt; when the relationship is a thing in its own right.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>associations</category>
      <category>database</category>
    </item>
    <item>
      <title>includes vs joins vs preload vs eager_load in Rails</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:45:29 +0000</pubDate>
      <link>https://dev.to/hasan-dev/includes-vs-joins-vs-preload-vs-eagerload-in-rails-5fop</link>
      <guid>https://dev.to/hasan-dev/includes-vs-joins-vs-preload-vs-eagerload-in-rails-5fop</guid>
      <description>&lt;p&gt;These four all deal with associations, and they're easy to mix up. The way I keep them straight is to ask two questions about each: what SQL does it run, and does it actually pull the associated records into memory? Once you can answer both, picking the right one is straightforward.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;SQL it generates&lt;/th&gt;
&lt;th&gt;Loads the association?&lt;/th&gt;
&lt;th&gt;Reach for it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;joins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INNER JOIN&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;You filter or sort by the associated table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate queries, one per association&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;You'll read the association and want it loaded separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eager_load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single LEFT OUTER JOIN&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;You need to read and filter by the association in one query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rails picks preload or eager_load&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The default for killing N+1 when you're not sure which you need&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  joins
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;joins&lt;/code&gt; builds a SQL join but does not load anything into memory. &lt;code&gt;User.joins(:posts).where(posts: { published: true })&lt;/code&gt; filters users by their posts cheaply. But if you then call &lt;code&gt;user.posts&lt;/code&gt; in Ruby, you get a fresh query for every user, which is the N+1 you were trying to avoid, because the posts were never loaded. Use &lt;code&gt;joins&lt;/code&gt; to filter and sort, not to display.&lt;/p&gt;

&lt;h2&gt;
  
  
  preload
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;preload&lt;/code&gt; always runs separate queries, one for the users and one for their posts, then stitches them together in memory. There is no join, so you can't reference the posts table in a &lt;code&gt;where&lt;/code&gt;. Try it and Rails will complain.&lt;/p&gt;

&lt;h2&gt;
  
  
  eager_load
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;eager_load&lt;/code&gt; forces a single LEFT OUTER JOIN and builds the association out of the joined rows. Unlike &lt;code&gt;preload&lt;/code&gt;, it still works when you filter on the joined table, because the join is right there in the query.&lt;/p&gt;

&lt;h2&gt;
  
  
  includes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;includes&lt;/code&gt; is the one I reach for most. You're telling Rails what you want ("load these associations, don't N+1 me") and letting it choose the strategy. By default it preloads with separate queries. The moment you reference the associated table in a &lt;code&gt;where&lt;/code&gt; or &lt;code&gt;order&lt;/code&gt;, it switches to &lt;code&gt;eager_load&lt;/code&gt; and runs the join instead. You usually don't have to think about which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it to work
&lt;/h2&gt;

&lt;p&gt;Two quick cases to make it stick.&lt;/p&gt;

&lt;p&gt;To list users along with their posts, use &lt;code&gt;User.includes(:posts)&lt;/code&gt;. You're displaying the association, so separate queries are fine and you've killed the N+1.&lt;/p&gt;

&lt;p&gt;To find only the users who have published posts, use &lt;code&gt;User.joins(:posts).where(posts: { published: true })&lt;/code&gt;. You're filtering by the association and you never need the post objects in memory, so there's no reason to load them. If you want to filter and then display those posts, use &lt;code&gt;includes&lt;/code&gt; together with &lt;code&gt;references&lt;/code&gt;, or just use &lt;code&gt;eager_load&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>What is a database transaction, and when do you reach for one in Rails?</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:24:25 +0000</pubDate>
      <link>https://dev.to/hasan-dev/what-is-a-database-transaction-and-when-do-you-reach-for-one-in-rails-31o2</link>
      <guid>https://dev.to/hasan-dev/what-is-a-database-transaction-and-when-do-you-reach-for-one-in-rails-31o2</guid>
      <description>&lt;p&gt;A transaction groups several database writes into one atomic unit. Either all of them commit, or none of them do. That is the guarantee you are buying: you never end up with half the work done and a database that contradicts itself.&lt;/p&gt;

&lt;p&gt;People recite the full ACID list (atomicity, consistency, isolation, durability), but the property I actually reach for day to day is atomicity. All or nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real scenario: checkout
&lt;/h2&gt;

&lt;p&gt;Placing an order looks like one action to the user, but under the hood it is several writes that all have to agree with each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;product: &lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrement!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say the line items save fine but the stock decrement blows up halfway through the loop. Without a transaction I now have an order that sold three units of something while only deducting one from inventory. With the transaction, the exception rolls the whole block back and I am left in a clean state, as if the order never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I watch for
&lt;/h2&gt;

&lt;p&gt;Rollback only fires on a raised exception. This is the one that bites people. &lt;code&gt;create&lt;/code&gt; returns false on a validation failure, it does not raise, so the transaction happily commits everything else around it. Use the bang versions inside the block (&lt;code&gt;create!&lt;/code&gt;, &lt;code&gt;save!&lt;/code&gt;, &lt;code&gt;update!&lt;/code&gt;) so a failure actually throws and triggers the rollback.&lt;/p&gt;

&lt;p&gt;Don't rescue the exception inside the block. If you wrap the body in a &lt;code&gt;begin/rescue&lt;/code&gt; and swallow the error, you have also swallowed the signal that tells the transaction to roll back, so it commits anyway. If you genuinely need to abort without an exception bubbling up to your callers, raise &lt;code&gt;ActiveRecord::Rollback&lt;/code&gt;. It rolls back quietly and does not re-raise.&lt;/p&gt;

&lt;p&gt;Keep slow external calls out of the transaction. An open transaction holds locks on the rows you have touched. If you put a Stripe charge or any HTTP request inside the block, you are holding those locks for the length of a network round trip. Under load that is how you get lock contention and a drained connection pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Stripe charge specifically is risky
&lt;/h2&gt;

&lt;p&gt;There are two problems. The first is the lock-holding one above. The second is worse: a charge is an external side effect, and you cannot roll it back. If the transaction commits and something downstream fails, or the transaction rolls back after Stripe already charged the card, your database and Stripe now disagree, and nothing reconciles that for you.&lt;/p&gt;

&lt;p&gt;The pattern I use is to do the local database work inside the transaction and handle the charge outside it. Confirm the payment through webhooks and reconcile against them, so a retry never double-charges the customer.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>database</category>
      <category>postgres</category>
      <category>activerecord</category>
    </item>
    <item>
      <title>How do you know you need a database index?</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:04:48 +0000</pubDate>
      <link>https://dev.to/hasan-dev/how-do-you-know-you-need-a-database-index-4c1h</link>
      <guid>https://dev.to/hasan-dev/how-do-you-know-you-need-a-database-index-4c1h</guid>
      <description>&lt;p&gt;I got asked this in an interview years ago, and I've asked it from the other side of the table since. I like it because the lazy answer ("index everything") is wrong, and the real skill is knowing where to look before you touch anything. So here's the whole loop: how I spot a column that needs an index, how I prove the index actually helped, and what it costs me to add one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which columns actually need one
&lt;/h2&gt;

&lt;p&gt;The candidates are columns you filter, sort, or join on. In SQL terms, anything in a WHERE, ORDER BY, JOIN, or GROUP BY on a table that's big or growing.&lt;/p&gt;

&lt;p&gt;A few I always check first:&lt;/p&gt;

&lt;p&gt;Foreign keys, like &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;order_id&lt;/code&gt;. In Rails these don't get an index automatically when you add the reference unless you ask for one, and they get hammered by association lookups and joins. This is the missing index I find most often.&lt;/p&gt;

&lt;p&gt;Lookup columns like &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;slug&lt;/code&gt;, and &lt;code&gt;token&lt;/code&gt;. These usually want a unique index anyway.&lt;/p&gt;

&lt;p&gt;Filter and sort columns like &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;created_at&lt;/code&gt;, the stuff your dashboards page over.&lt;/p&gt;

&lt;p&gt;None of it matters at small scale. A sequential scan over 500 rows is instant. The pain shows up at a few million rows, when an endpoint that felt fine in development starts timing out in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measure whether it helped
&lt;/h2&gt;

&lt;p&gt;Mostly EXPLAIN ANALYZE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I read in the output. A &lt;code&gt;Seq Scan&lt;/code&gt; on a big table is the red flag: Postgres is reading every row to answer the query. After I add the index, I want to see that become an &lt;code&gt;Index Scan&lt;/code&gt; or a &lt;code&gt;Bitmap Index Scan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I compare the actual time before and after, not just the plan shape. I also check estimated rows against actual rows. When those are far apart, the planner is running on stale statistics, and a quick &lt;code&gt;ANALYZE&lt;/code&gt; sometimes fixes the query with no index at all.&lt;/p&gt;

&lt;p&gt;The local plan only goes so far. To find what's worth fixing, I look at production: &lt;code&gt;pg_stat_statements&lt;/code&gt; for the genuinely expensive queries, plus whatever APM is running (New Relic, Skylight, Datadog) and the query timings in the Rails log. It's easy to lovingly optimize a query that runs twice a day while ignoring the one that runs ten thousand times an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexes aren't free
&lt;/h2&gt;

&lt;p&gt;This is the part people skip. Every index costs disk space, and it slows down writes, because every INSERT, UPDATE, and DELETE has to keep the index current. On a write-heavy table that adds up fast.&lt;/p&gt;

&lt;p&gt;So I don't index on a hunch. I index columns I can show are being queried, and I drop indexes nobody uses. An unused index is pure cost. It slows your writes and gives you nothing back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical one: &lt;code&gt;Order.where(user_id: id, status: "paid")&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I'd add a composite index on &lt;code&gt;[:user_id, :status]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Column order is the whole game here. &lt;code&gt;user_id&lt;/code&gt; goes first because it's the selective, always-present equality filter. The B-tree narrows to one user's orders, then &lt;code&gt;status&lt;/code&gt; filters within that small set.&lt;/p&gt;

&lt;p&gt;There's a bonus: because &lt;code&gt;user_id&lt;/code&gt; is the leftmost column, this same index also covers queries that filter on &lt;code&gt;user_id&lt;/code&gt; alone, so you don't need a second index just for it.&lt;/p&gt;

&lt;p&gt;Reversing it to &lt;code&gt;[:status, :user_id]&lt;/code&gt; would be worse. &lt;code&gt;status&lt;/code&gt; has only a handful of distinct values, so leading with it barely narrows the search before it gets to &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>performance</category>
      <category>interview</category>
      <category>database</category>
    </item>
    <item>
      <title>The N+1 Query Problem</title>
      <dc:creator>Hassan Farooq</dc:creator>
      <pubDate>Sun, 21 Jun 2026 19:18:46 +0000</pubDate>
      <link>https://dev.to/hasan-dev/rails-interview-1-the-n1-query-problem-5ei8</link>
      <guid>https://dev.to/hasan-dev/rails-interview-1-the-n1-query-problem-5ei8</guid>
      <description>&lt;p&gt;You ship a &lt;code&gt;/posts&lt;/code&gt; index page. It renders 50 posts, and for each one it shows the author's name with &lt;code&gt;post.author.name&lt;/code&gt;. QA says the page is slow, and the logs are full of repetitive SQL.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is this problem called, and why is it happening?&lt;/li&gt;
&lt;li&gt;How would you detect it, in dev and in a running app?&lt;/li&gt;
&lt;li&gt;How do you fix it, and what's the difference between the main fixing strategies?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Answer: N+1 queries
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is and why it happens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the N+1 query problem. One query loads the 50 posts, then Active Record fires one more query per post to load &lt;code&gt;post.author&lt;/code&gt;. That's 1 + N queries (1 for posts, 50 for authors) when it could be 2, or even 1. Active Record associations are lazy by default, so the author isn't loaded until you call &lt;code&gt;post.author&lt;/code&gt;. Do that inside a loop and you get a round trip per record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to detect it&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dev logs: you'll see the same &lt;code&gt;SELECT ... FROM authors WHERE id = ?&lt;/code&gt; over and over with different IDs. That repetition is the tell.&lt;/li&gt;
&lt;li&gt;The Bullet gem: built for this. It warns you in dev when you should add eager loading, and also when you're eager-loading something you don't need.&lt;/li&gt;
&lt;li&gt;An APM in production (Sentry, New Relic, Scout): flags endpoints firing a lot of queries.&lt;/li&gt;
&lt;li&gt;Tests: assert on query count with something like &lt;code&gt;assert_queries&lt;/code&gt; or an RSpec matcher so CI catches a regression before it ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to fix it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eager-load the association:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# N+1&lt;/span&gt;
&lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
&lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 1 + 50 queries&lt;/span&gt;

&lt;span class="c1"&gt;# Fixed&lt;/span&gt;
&lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 2 queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are four tools, and they don't do the same thing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loads the association in a separate query and matches it in memory&lt;/td&gt;
&lt;td&gt;You just need the data and aren't filtering or ordering by the association&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eager_load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt; that loads everything in a single query&lt;/td&gt;
&lt;td&gt;You need to filter or order by the association &lt;em&gt;and&lt;/em&gt; read its attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;includes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defaults to &lt;code&gt;preload&lt;/code&gt;, but switches to &lt;code&gt;eager_load&lt;/code&gt; if you reference the association in a &lt;code&gt;where&lt;/code&gt; or &lt;code&gt;order&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Your usual default. Let Rails decide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;joins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;INNER JOIN&lt;/code&gt; for filtering or sorting. Does not load the association into memory&lt;/td&gt;
&lt;td&gt;You need to filter by the association but don't read its attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trap: &lt;code&gt;joins&lt;/code&gt; alone does not preload. If you &lt;code&gt;joins(:author)&lt;/code&gt; and then call &lt;code&gt;p.author.name&lt;/code&gt; in the loop, you're right back to N+1. Reach for &lt;code&gt;includes&lt;/code&gt; or &lt;code&gt;eager_load&lt;/code&gt; when you actually read the association's attributes.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>database</category>
      <category>performance</category>
      <category>interview</category>
    </item>
  </channel>
</rss>
