<?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: Syed Aslam</title>
    <description>The latest articles on DEV Community by Syed Aslam (@syedaslam).</description>
    <link>https://dev.to/syedaslam</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%2F34112%2Fb88df536-9468-483a-a651-cef3dddd38bf.jpeg</url>
      <title>DEV Community: Syed Aslam</title>
      <link>https://dev.to/syedaslam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syedaslam"/>
    <language>en</language>
    <item>
      <title>Git Worktrees: Multiple Branches Without the Checkout Shuffle</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Wed, 22 Jul 2026 21:39:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/git-worktrees-multiple-branches-without-the-checkout-shuffle-3jk0</link>
      <guid>https://dev.to/syedaslam/git-worktrees-multiple-branches-without-the-checkout-shuffle-3jk0</guid>
      <description>&lt;p&gt;Working on more than one Git branch often creates an awkward routine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop what you are doing.&lt;/li&gt;
&lt;li&gt;Stash incomplete changes.&lt;/li&gt;
&lt;li&gt;Check out another branch.&lt;/li&gt;
&lt;li&gt;Investigate or fix something.&lt;/li&gt;
&lt;li&gt;Switch back.&lt;/li&gt;
&lt;li&gt;Restore the stash and hope nothing was misplaced.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gets especially painful with stacked pull requests, urgent fixes, long-running test suites, or reviews that require comparing several versions of the same code.&lt;/p&gt;

&lt;p&gt;Git worktrees offer a cleaner approach. They let you check out multiple branches from one repository into separate directories at the same time.&lt;/p&gt;

&lt;p&gt;I first paid attention to worktrees after noticing AI coding tools using them to run multiple agents against the same repository in parallel, each in its own checkout, without overwriting the others’ working files, with no need to push anything to the cloud just to isolate it. That was reason enough to learn how the mechanism actually works.&lt;/p&gt;

&lt;p&gt;I put them to real use during a stacked-branch repair: a parent branch needed fixing, and a child branch built on top of it needed rebasing afterward. I was running AI coding tools alongside my own changes, and repeatedly stashing and switching made it increasingly difficult to track which branch and working-tree state I was looking at. Moving the parent and child into their own worktrees, leaving my original checkout, including its untracked review notes, untouched, made the rebase far easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;A normal Git repository combines two kinds of state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared repository state:&lt;/strong&gt; commits, branches, tags, remote references, and repository configuration by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout state:&lt;/strong&gt; the current branch, working files, staged changes, and in-progress operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A worktree adds another checkout while continuing to use the same shared repository data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gbzpg43eimske0l4gjg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gbzpg43eimske0l4gjg.png" alt="A diagram showing the shared Git repository state connected to multiple independent worktrees" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each worktree has its own checked-out files, index, and &lt;code&gt;HEAD&lt;/code&gt;. Commits and branch references remain shared.&lt;/p&gt;

&lt;p&gt;This means one directory can remain on a base branch while another contains a feature branch and a third contains its dependent child branch. There is no need to stash or repeatedly switch between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Suppose a repository lives in a directory named &lt;code&gt;project&lt;/code&gt;, and a feature branch needs to be inspected without disturbing the current checkout.&lt;/p&gt;

&lt;p&gt;First, refresh the remote references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a worktree for an existing local branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../project-feature feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting directories might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workspace/
├── project/          main checkout
└── project-feature/  feature branch checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;../&lt;/code&gt; is what keeps it a sibling. Drop it and run &lt;code&gt;git worktree add project-feature feature-branch&lt;/code&gt; from inside &lt;code&gt;project&lt;/code&gt;, and Git will happily create the new worktree nested inside your current directory instead, where it then shows up as an untracked folder in &lt;code&gt;project&lt;/code&gt;'s own &lt;code&gt;git status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both directories belong to the same Git repository, but each has independent working files.&lt;/p&gt;

&lt;p&gt;Alternatively, to create a new local branch from a remote branch while setting up a worktree in a dedicated directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; repair/feature &lt;span class="se"&gt;\&lt;/span&gt;
  ../project-repair &lt;span class="se"&gt;\&lt;/span&gt;
  origin/feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now enter the new directory and work normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-repair
git status
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="c"&gt;# or npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commits created there are immediately available to the other worktrees because they share the same Git object database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspecting active worktrees
&lt;/h2&gt;

&lt;p&gt;List the registered worktrees with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/path/to/project          1a2b3c4 [main]
/path/to/project-feature  5d6e7f8 [feature-branch]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scripts or detailed troubleshooting, use the machine-readable form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is shared and what is isolated?
&lt;/h2&gt;

&lt;p&gt;Understanding the boundary prevents most surprises.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Shared between worktrees?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commits and Git objects&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local branch references&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote references&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repository configuration&lt;/td&gt;
&lt;td&gt;Shared by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stash list&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current branch and &lt;code&gt;HEAD&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checked-out files&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging index&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Untracked files&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-progress rebase or merge&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build output inside the directory&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shared stash list is easy to overlook. Running &lt;code&gt;git stash&lt;/code&gt; stashes uncommitted changes from the current worktree, but the stash entry is stored in the shared Git repository state. This means &lt;code&gt;git stash list&lt;/code&gt;, &lt;code&gt;git stash apply&lt;/code&gt;, and &lt;code&gt;git stash pop&lt;/code&gt; operate on the same shared stash stack from every worktree. Using descriptive messages helps avoid popping a stash into the wrong branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feature branch before conflict resolution"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repository configuration is shared by default. Git can also maintain worktree-specific settings when &lt;code&gt;extensions.worktreeConfig&lt;/code&gt; is enabled and values are written using &lt;code&gt;git config --worktree&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Untracked files are the other easy thing to overlook, and the one that stalls a new worktree the fastest. A worktree only contains files Git tracks; anything ignored or generated locally has to be created again. In a Rails app, that often includes &lt;code&gt;config/master.key&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, local &lt;code&gt;database.yml&lt;/code&gt; overrides, &lt;code&gt;node_modules&lt;/code&gt;, and any worktree-local bundle path such as &lt;code&gt;vendor/bundle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-feature
&lt;span class="nb"&gt;cp&lt;/span&gt; ../project/config/master.key config/master.key
&lt;span class="nb"&gt;cp&lt;/span&gt; ../project/.env .env
bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Symlinking instead of copying keeps credentials and environment files in sync automatically, which is useful when every worktree should run against the same local environment. A relative symlink target resolves from the symlink's own directory, not from your shell's current directory, so &lt;code&gt;config/master.key&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; need different numbers of &lt;code&gt;../&lt;/code&gt; to reach the same place. Use an absolute path instead to avoid that arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROJECT_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/config/master.key"&lt;/span&gt; config/master.key
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/.env"&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only symlink files you want shared. A worktree deliberately testing a different &lt;code&gt;.env&lt;/code&gt; or database configuration should keep its own copy instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most important restriction
&lt;/h2&gt;

&lt;p&gt;Git generally prevents the same branch from being checked out in two worktrees simultaneously.&lt;/p&gt;

&lt;p&gt;Attempting it produces an error similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: 'feature-branch' is already checked out at '/path/to/project-feature'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This restriction protects the branch from being moved independently by two checkouts. When a branch is already checked out elsewhere, you have four main options: use that existing worktree, create another branch, use a detached worktree for inspection, or remove the obsolete worktree.&lt;/p&gt;

&lt;p&gt;When another view of the same commit is needed, create a detached worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="nt"&gt;--detach&lt;/span&gt; ../project-inspect origin/feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detached worktrees are useful for inspection and testing. If commits are created there, attach them to a branch before relying on them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; investigation/result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why worktrees are useful for stacked pull requests
&lt;/h2&gt;

&lt;p&gt;Consider a stack with a base branch, a parent feature, and a child feature. The child must be rebased after correcting the parent.&lt;/p&gt;

&lt;p&gt;Without worktrees, the process often becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stash → switch to parent → repair → test → switch to child
      → rebase → resolve → test → restore previous work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With worktrees, each branch remains available in its own directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workspace/
├── project-base/
├── project-parent/
└── project-child/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repair and test the parent in its worktree.&lt;/li&gt;
&lt;li&gt;Commit and push the corrected parent.&lt;/li&gt;
&lt;li&gt;Rebase the child in its own worktree.&lt;/li&gt;
&lt;li&gt;Consult the parent directory while resolving conflicts.&lt;/li&gt;
&lt;li&gt;Test the exact child commit intended for push.&lt;/li&gt;
&lt;li&gt;Repeat for the next descendant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is safer than repeatedly switching branches because the corrected parent remains visible throughout conflict resolution. It also reduces the chance of accidentally restoring stale code from the child, while eliminating the repeated dependency and build churn caused by switching a single directory between different branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reusable stacked-branch setup
&lt;/h2&gt;

&lt;p&gt;Create dedicated worktrees for a parent and child branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin

git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; parent-branch &lt;span class="se"&gt;\&lt;/span&gt;
  ../project-parent &lt;span class="se"&gt;\&lt;/span&gt;
  origin/parent-branch

git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; child-branch &lt;span class="se"&gt;\&lt;/span&gt;
  ../project-child &lt;span class="se"&gt;\&lt;/span&gt;
  origin/child-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After repairing and committing the parent, rebase the child:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-child
git rebase parent-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During conflicts, the parent worktree remains available as a trustworthy reference. After resolving and testing, the rewritten child branch can be pushed safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin child-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parallel testing and investigation
&lt;/h2&gt;

&lt;p&gt;Worktrees are also useful when no branch rewrite is involved.&lt;/p&gt;

&lt;p&gt;They make it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a long test suite on one branch while editing another.&lt;/li&gt;
&lt;li&gt;Compare generated output between two branches.&lt;/li&gt;
&lt;li&gt;Review a pull request without disturbing unfinished work.&lt;/li&gt;
&lt;li&gt;Keep a production hotfix separate from feature development.&lt;/li&gt;
&lt;li&gt;Test multiple dependency or framework upgrade branches.&lt;/li&gt;
&lt;li&gt;Open parent and child implementations side by side in an editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each worktree has separate filesystem output, so test artifacts and build products do not overwrite those in another worktree unless they use an external shared service or cache.&lt;/p&gt;

&lt;p&gt;Because branches live in separate directories, you can also use standard terminal diffing tools to compare implementations directly across worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compare specific files between parent and child worktrees&lt;/span&gt;
diff &lt;span class="nt"&gt;-u&lt;/span&gt; ../project-parent/src/service.rb ../project-child/src/service.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Databases, Redis instances, containers, ports, and global caches are not automatically isolated merely because the source directories are separate. Parallel test environments may still need distinct database names, ports, or namespaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging a worktree's branch back
&lt;/h2&gt;

&lt;p&gt;Merging only picks up what's committed, so confirm the feature worktree is clean first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-feature
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit or stash anything outstanding. An uncommitted change sitting in the feature worktree won't be part of the merge, and it's easy to assume it was included just because the branch was.&lt;/p&gt;

&lt;p&gt;While still in the feature worktree, rebasing onto main first keeps the merge itself trivial, often a fast-forward instead of a merge commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolve any conflicts here, in the feature worktree, rather than during the merge itself.&lt;/p&gt;

&lt;p&gt;Because commits and branch references are shared across worktrees, merging one worktree's branch into another doesn't require pushing or fetching first. From the worktree that has the target branch checked out, commonly the main worktree, merge directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project
git switch main
git merge feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the workflow goes through a pull request instead, push the branch, merge on GitHub, then run &lt;code&gt;git pull&lt;/code&gt; in the worktree tracking &lt;code&gt;main&lt;/code&gt; to bring the merge commit in locally.&lt;/p&gt;

&lt;p&gt;Once the branch is merged, its worktree has done its job and can be removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing a worktree safely
&lt;/h2&gt;

&lt;p&gt;Before removing a worktree, check for local changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-feature
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then move to another worktree and remove it through Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project
git worktree remove ../project-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;git worktree remove&lt;/code&gt; is preferable to deleting the directory manually because Git also removes its internal registration.&lt;/p&gt;

&lt;p&gt;Removing the worktree doesn't delete the branch it had checked out. Branches are shared repository state, not checkout state. Once a branch is merged and its worktree is gone, delete the branch itself if it's no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a directory was deleted outside Git, clean up stale metadata with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Force removal is available, but it can discard modified and untracked files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove &lt;span class="nt"&gt;--force&lt;/span&gt; ../project-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it only after confirming that nothing in the worktree needs to be preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worktrees are not clones
&lt;/h2&gt;

&lt;p&gt;A second clone also provides another checkout, but it duplicates repository objects and has independent local branches, tags, stashes, and configuration. Keeping those clones synchronized requires additional fetching and coordination.&lt;/p&gt;

&lt;p&gt;A worktree is lighter and more tightly connected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Objects are stored once.&lt;/li&gt;
&lt;li&gt;New commits are immediately visible everywhere.&lt;/li&gt;
&lt;li&gt;Local branches and tags are shared.&lt;/li&gt;
&lt;li&gt;Repository configuration is shared by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use separate clones when true repository-level isolation is required. Use worktrees when the goal is simply to work with multiple branches from the same repository at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on project layout: Bare repositories
&lt;/h3&gt;

&lt;p&gt;While the examples in this post place worktrees in sibling directories (&lt;code&gt;../project-parent&lt;/code&gt;), developers who use worktrees heavily often clone repositories as bare repositories instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;project
&lt;span class="nb"&gt;cd &lt;/span&gt;project

git clone &lt;span class="nt"&gt;--bare&lt;/span&gt; git@github.com:user/project.git .bare

git &lt;span class="nt"&gt;--git-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.bare config remote.origin.fetch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'+refs/heads/*:refs/remotes/origin/*'&lt;/span&gt;
git &lt;span class="nt"&gt;--git-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.bare fetch origin

git &lt;span class="nt"&gt;--git-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.bare worktree add main main
git &lt;span class="nt"&gt;--git-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.bare worktree add feature feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare clone places the remote's branch heads directly under local branches and does not create the ordinary &lt;code&gt;origin/*&lt;/code&gt; remote-tracking references on its own. The explicit fetch refspec above restores that familiar layout, which the &lt;code&gt;git fetch origin&lt;/code&gt; and &lt;code&gt;origin/feature-branch&lt;/code&gt; references used earlier in this post depend on.&lt;/p&gt;

&lt;p&gt;The resulting layout keeps all worktree checkouts cleanly separated under a single workspace folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── .bare/
├── main/
└── feature/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike a normal clone, a bare repository has no default working tree of its own, acting purely as the shared repository storage for linked worktrees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical safety rules
&lt;/h2&gt;

&lt;p&gt;A few habits make worktrees predictable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use clear directory names that identify the branch or task.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git status&lt;/code&gt; before rebasing, resetting, or removing a worktree.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git worktree list&lt;/code&gt; before deleting or renaming branches.&lt;/li&gt;
&lt;li&gt;Remember that branch references are shared: committing, resetting, rebasing, or otherwise moving a branch in one worktree is immediately visible from the others.&lt;/li&gt;
&lt;li&gt;Avoid manually deleting worktree directories.&lt;/li&gt;
&lt;li&gt;Keep important backup branches until rewritten branches have been pushed and verified.&lt;/li&gt;
&lt;li&gt;Test from the worktree containing the exact commit intended for push.&lt;/li&gt;
&lt;li&gt;Isolate external services when running worktrees concurrently.&lt;/li&gt;
&lt;li&gt;Open each worktree in its own editor or IDE window so language server (LSP) indexes remain isolated.&lt;/li&gt;
&lt;li&gt;Remove finished worktrees so the workspace does not become cluttered.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A worktree is another independent checkout of the same Git repository, allowing multiple branches to remain open simultaneously without cloning the repository multiple times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It replaces the repeated stash-and-switch routine with a stable directory per branch. That makes reviews, stacked pull requests, difficult rebases, parallel testing, and urgent fixes easier to reason about and much less likely to disturb unrelated work.&lt;/p&gt;

&lt;p&gt;For many multi-branch workflows, the most useful first commands are simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../project-feature feature-branch
git worktree list
git worktree remove ../project-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that mental model clicks, worktrees become one of Git's most practical tools.&lt;/p&gt;

</description>
      <category>git</category>
      <category>devtools</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Why present? Can Hide SQL in Rails</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Fri, 24 Apr 2026 13:46:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/why-present-can-hide-sql-in-rails-3fgf</link>
      <guid>https://dev.to/syedaslam/why-present-can-hide-sql-in-rails-3fgf</guid>
      <description>&lt;p&gt;A lot of Rails performance problems do not start with obviously expensive code, but with code that looks harmless.&lt;/p&gt;

&lt;p&gt;For example:&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;if&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;present?&lt;/span&gt;
  &lt;span class="c1"&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 reads like a simple boolean check, ordinary Ruby. and not like a database operation. But on an unloaded Active Record association, that line may issue SQL.&lt;/p&gt;

&lt;p&gt;That is what makes it dangerous. Not the cost of one call, but the mismatch between how the code reads and what the system actually does.&lt;/p&gt;

&lt;p&gt;In Rails, some predicates are not purely local questions. They can cross the database boundary to answer what looks like an in-memory check.&lt;/p&gt;

&lt;p&gt;Once you see that, a lot of "mysterious" performance issues stop looking mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is easy to&amp;nbsp;miss
&lt;/h2&gt;

&lt;p&gt;The method name is the trap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; sounds like a lightweight predicate. In most Ruby code, that intuition is fine. You ask a question about a value that already exists in memory, and you get an answer.&lt;/p&gt;

&lt;p&gt;Associations break that expectation.&lt;/p&gt;

&lt;p&gt;When you call:&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;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;present?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails may be able to answer from memory if &lt;code&gt;line_items&lt;/code&gt; is already loaded. If not loaded, Rails has to hit the database to decide whether the association is empty.&lt;/p&gt;

&lt;p&gt;The same line can behave differently depending on context the line itself does not show. That is a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this becomes expensive
&lt;/h2&gt;

&lt;p&gt;One hidden query usually doesn't matter. A hundred do.&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;orders&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;order&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&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;present?&lt;/span&gt;
  &lt;span class="c1"&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;Nothing about the loop looks suspicious. There is no &lt;code&gt;where&lt;/code&gt;, no &lt;code&gt;find_by&lt;/code&gt;, no explicit query call. The code reads cleanly. But if line_items has not been preloaded, that can turn into one query per order.&lt;/p&gt;

&lt;p&gt;This is one reason &lt;code&gt;N+1&lt;/code&gt; issues survive in Rails applications. The expensive part often hides behind method calls that do not advertise that they might perform I/O.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is happening under the&amp;nbsp;hood
&lt;/h2&gt;

&lt;p&gt;The exact implementation details are less important than the shape of the behaviour.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; delegates to &lt;code&gt;blank?&lt;/code&gt;. In an association, answering whether it is blank eventually depends on whether it is empty. If the association is not loaded, Active Record has to query the database to answer that question.&lt;/p&gt;

&lt;p&gt;So the issue is not really &lt;code&gt;present?&lt;/code&gt; versus &lt;code&gt;blank?&lt;/code&gt;. The real issue is that predicate-like code can quietly depend on the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to write&amp;nbsp;instead
&lt;/h2&gt;

&lt;p&gt;The fix is on intent. If your real question is &lt;em&gt;"do related records exist in the database?"&lt;/em&gt;, write that directly:&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;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;exist?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the boundary explicit. A reader immediately knows this line may hit the database.&lt;/p&gt;

&lt;p&gt;If your real intention is to use the records in memory, preload them 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;orders&lt;/span&gt; &lt;span class="o"&gt;=&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;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:line_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;orders&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;order&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&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;present?&lt;/span&gt;
  &lt;span class="c1"&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;Now the check is operating on already-loaded data. The code reads more honestly.&lt;/p&gt;

&lt;p&gt;That is the standard I keep coming back to: if a line might hit the database, it should either say so directly or sit inside a structure that makes the loading strategy visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is bigger than&amp;nbsp;&lt;code&gt;present?&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; is just an easy example. Rails has other methods with the same shape. &lt;code&gt;any?&lt;/code&gt;, &lt;code&gt;empty?&lt;/code&gt;, and sometimes &lt;code&gt;size&lt;/code&gt; can all blur the line between in-memory logic and database-backed behaviour when they are called on associations.&lt;/p&gt;

&lt;p&gt;The details vary, but the lesson does not.&lt;/p&gt;

&lt;p&gt;If a method looks local but depends on load state, it is worth pausing before scattering it through hot paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that&amp;nbsp;helps
&lt;/h2&gt;

&lt;p&gt;In long-lived Rails systems, I try to make query boundaries visible.&lt;/p&gt;

&lt;p&gt;Not because convenience methods are bad. But, because hidden I/O makes code harder to reason about. They hide costs and make reviews weaker, and let performance issues pass as innocent object logic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; is fine when the data is already in memory. It becomes a smell when it hides the moment your code stops being local.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>performance</category>
    </item>
    <item>
      <title>present? and the Hidden Query Boundary</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/present-and-the-hidden-query-boundary-51am</link>
      <guid>https://dev.to/syedaslam/present-and-the-hidden-query-boundary-51am</guid>
      <description>&lt;p&gt;This looks harmless:&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;if&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;present?&lt;/span&gt;
  &lt;span class="c1"&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;It reads like a pure boolean check. In ActiveRecord associations, it sometimes isn't.&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;code&gt;present?&lt;/code&gt; delegates to &lt;code&gt;blank?&lt;/code&gt;, which for un-loaded associations cascades down to &lt;code&gt;empty?&lt;/code&gt;. When &lt;code&gt;empty?&lt;/code&gt; is called, ActiveRecord may hit the database to determine if records exist. The method name suggests a local predicate. The behavior sometimes crosses the database boundary.&lt;/p&gt;

&lt;p&gt;Without preloading, this simple check can become surprisingly expensive in aggregate:&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;orders&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;order&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&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;present?&lt;/span&gt;
  &lt;span class="c1"&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;In a loop, it becomes one query per order. It scales with record count. It doesn’t look like I/O at the call site.&lt;/p&gt;

&lt;p&gt;In long-lived systems, I try to make database boundaries obvious in code. If a predicate can hit the database, I want that to be visible. Using &lt;code&gt;exists?&lt;/code&gt; makes the I/O explicit. Alternatively, preloading the association upfront ensures that &lt;code&gt;present?&lt;/code&gt; remains a purely in-memory check. Being explicit about these access patterns removes ambiguity.&lt;/p&gt;

&lt;p&gt;Convenience helpers are useful. But when they conceal I/O, they also conceal cost. In small systems, that rarely matters. In mature ones, it adds up.&lt;/p&gt;

&lt;p&gt;This pattern appears frequently when diagnosing performance in &lt;a href="https://syedaslam.com/posts/where-rails-apps-actually-slow-down" rel="noopener noreferrer"&gt;long-lived Rails systems&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activerecord</category>
      <category>performance</category>
    </item>
    <item>
      <title>When ||= Betrays Your Memoization</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Sat, 21 Feb 2026 10:42:42 +0000</pubDate>
      <link>https://dev.to/syedaslam/when-betrays-your-memoization-3jdk</link>
      <guid>https://dev.to/syedaslam/when-betrays-your-memoization-3jdk</guid>
      <description>&lt;p&gt;Memoization in Ruby often looks harmless:&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;active?&lt;/span&gt;
  &lt;span class="vi"&gt;@active&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;compute_active_flag&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’s clean. It’s idiomatic. It avoids duplicate work.&lt;/p&gt;

&lt;p&gt;For years, this pattern feels safe.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;||=&lt;/code&gt; does not mean “if unset.”&lt;br&gt;
It means “if falsey.”&lt;/p&gt;

&lt;p&gt;And in long-lived systems, that difference matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  The subtle behavior
&lt;/h2&gt;

&lt;p&gt;This:&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="vi"&gt;@active&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;compute_active_flag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means Ruby assigns only when &lt;code&gt;@active&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, since &lt;code&gt;nil&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; are the only falsey values in Ruby:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@active&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt; -&amp;gt; compute&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@active&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; -&amp;gt; compute&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@active&lt;/code&gt; is truthy -&amp;gt; reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Memoization with &lt;code&gt;||=&lt;/code&gt; is only reliable when the memoized value is guaranteed to become truthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this causes confusion over time
&lt;/h2&gt;

&lt;p&gt;In one system I worked on, a boolean flag was memoized exactly like this.&lt;/p&gt;

&lt;p&gt;At first, the flag was effectively “unset or true,” and everything worked.&lt;/p&gt;

&lt;p&gt;Later, the domain evolved. &lt;code&gt;false&lt;/code&gt; became an explicit, meaningful result based on new business rules.&lt;/p&gt;

&lt;p&gt;That’s when it got strange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the method recomputed unexpectedly, silently destroying performance if &lt;code&gt;compute_active_flag&lt;/code&gt; was expensive&lt;/li&gt;
&lt;li&gt;tests became noisy and inconsistent&lt;/li&gt;
&lt;li&gt;code readers assumed memoization was happening, because the syntax looked memoized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code looked correct.&lt;br&gt;
The semantics weren’t.&lt;/p&gt;

&lt;p&gt;That gap is where long-lived systems accumulate friction.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;As systems mature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flags gain new states&lt;/li&gt;
&lt;li&gt;defaults become explicit&lt;/li&gt;
&lt;li&gt;“maybe missing” becomes “intentionally false” (or sometimes intentionally &lt;code&gt;nil&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But &lt;code&gt;||=&lt;/code&gt; never changes its behavior. It keeps encoding a truthiness assumption that may no longer fit your domain.&lt;/p&gt;

&lt;p&gt;This isn’t a Ruby flaw.&lt;br&gt;
It’s a reminder that convenience operators carry semantics.&lt;/p&gt;
&lt;h2&gt;
  
  
  Safer patterns
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;false&lt;/code&gt; is valid, but &lt;code&gt;nil&lt;/code&gt; still means “unset”:&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;active?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="vi"&gt;@active&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
  &lt;span class="vi"&gt;@active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compute_active_flag&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;nil&lt;/code&gt; are legitimate cached values, guard on definition instead using &lt;code&gt;defined?&lt;/code&gt; (which is slightly faster, as it's evaluated by the parser rather than as a method call) or &lt;code&gt;instance_variable_defined?&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;def&lt;/span&gt; &lt;span class="nf"&gt;active?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="vi"&gt;@active&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;defined?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compute_active_flag&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 &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;nil&lt;/code&gt; are preserved as cached results, and behavior matches intent.&lt;/p&gt;

&lt;p&gt;It’s a few more characters.&lt;br&gt;
It’s also semantically honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;Most issues in long-running systems aren’t dramatic failures.&lt;/p&gt;

&lt;p&gt;They’re small mismatches between yesterday’s assumptions and today’s domain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;||=&lt;/code&gt; is a perfect example: convenient, idiomatic, and excellent in the right context.&lt;/p&gt;

&lt;p&gt;But when &lt;code&gt;false&lt;/code&gt; or &lt;code&gt;nil&lt;/code&gt; becomes meaningful, tiny semantics compound.&lt;/p&gt;

&lt;p&gt;Explicitness costs a little now.&lt;br&gt;
It usually saves much more later.&lt;/p&gt;




&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/3.3/syntax/assignment_rdoc.html" rel="noopener noreferrer"&gt;Ruby assignment syntax (&lt;code&gt;||=&lt;/code&gt; / &lt;code&gt;&amp;amp;&amp;amp;=&lt;/code&gt; behavior)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/3.2/syntax/miscellaneous_rdoc.html" rel="noopener noreferrer"&gt;Ruby &lt;code&gt;defined?&lt;/code&gt; docs (including precedence notes)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/3.4.1/Object.html" rel="noopener noreferrer"&gt;Ruby &lt;code&gt;Object#instance_variable_defined?&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://denisdefreyne.com/articles/2024-memoization/" rel="noopener noreferrer"&gt;Denis Defreyne: &lt;em&gt;The intricacies of implementing memoization in Ruby&lt;/em&gt; (Nov 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rubocop/ruby-style-guide" rel="noopener noreferrer"&gt;RuboCop Ruby Style Guide note on &lt;code&gt;||=&lt;/code&gt; with booleans&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>memoization</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stimulus Is Boring — and That’s Why It Works</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Tue, 03 Feb 2026 13:44:10 +0000</pubDate>
      <link>https://dev.to/syedaslam/stimulus-is-boring-and-thats-why-it-works-3mp</link>
      <guid>https://dev.to/syedaslam/stimulus-is-boring-and-thats-why-it-works-3mp</guid>
      <description>&lt;p&gt;People expect their frontend tools to feel powerful. They want velocity, elegance, leverage—some sense that a new layer will unlock a faster future. In that context, Stimulus can feel underwhelming. It’s small. It’s constrained. It doesn’t promise much.&lt;/p&gt;

&lt;p&gt;That gap between expectation and experience can feel like a warning. In a hype-driven ecosystem, “boring” reads like failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But that boredom isn’t a flaw. It’s the point.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When people call Stimulus boring, they often mean it as a dismissal. But “boring” in this context doesn’t mean outdated or weak. It means it has a limited surface area. It prefers explicit behavior over cleverness. Its failure modes are predictable. And it’s easy to delete.&lt;/p&gt;

&lt;p&gt;That last one matters more than most people admit. Tools you can remove without regret are tools that age well.&lt;/p&gt;

&lt;p&gt;The problem Stimulus is actually solving isn't the one people expect. Most Rails apps are not SPAs. Most UI behavior isn’t a complex state machine. It’s forms, toggles, modals, progressive enhancements. Most teams want fewer moving parts, lower cognitive load, and onboarding that doesn’t require a front-end renaissance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stimulus isn’t trying to “win the frontend.” It’s trying to get out of the way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s why it shines in mature codebases. In long-lived Rails apps. In teams with mixed skill levels and shifting ownership. In systems optimized for correctness and change safety. In places where deleting code matters more than adding features.&lt;/p&gt;

&lt;p&gt;Stimulus keeps JavaScript close to the HTML it enhances. That sounds small until you’re in the middle of a refactor, or hunting a production bug, or triaging an incident. Keeping behavior near the markup makes the whole system easier to reason about when it matters most. No benchmarks needed. Just judgment earned over years of maintaining real systems.&lt;/p&gt;

&lt;p&gt;None of this is a dismissal of modern frontend frameworks. React, Vue, and friends solve different problems. They optimize for expressiveness, for building rich, interactive experiences where the client is the product. &lt;strong&gt;Stimulus optimizes for longevity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most Rails apps don't fail because the frontend wasn't powerful enough. &lt;strong&gt;They fail because the system became too hard to reason about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That difficulty compounds over time. Dependency churn. Tooling fatigue. Cognitive overhead. Frontend rewrites no one budgets for. Stimulus's strength is that it rarely forces a rewrite. It doesn't create a parallel universe inside your app. It can be incrementally removed. It quietly resists the kind of complexity that only shows its cost years later.&lt;/p&gt;

&lt;p&gt;That doesn't make it a universal choice. Stimulus is the wrong tool for highly interactive, state-heavy UIs. It's the wrong choice when your primary complexity is client-side, or when your team is intentionally betting on frontend specialization. It doesn't pretend otherwise, and neither should we.&lt;/p&gt;

&lt;p&gt;What it offers instead is something durable: the quiet value of boring tools. They survive trends. They age without ceremony. They reward teams who plan to be around.&lt;/p&gt;

&lt;p&gt;Stimulus isn’t exciting. It’s dependable. And in mature systems, that’s usually the harder thing to build.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Escaping the AMS Trap: How We Benchmarked Our Way to Alba</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Tue, 20 Jan 2026 12:35:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/escaping-the-ams-trap-how-we-benchmarked-our-way-to-alba-3gcc</link>
      <guid>https://dev.to/syedaslam/escaping-the-ams-trap-how-we-benchmarked-our-way-to-alba-3gcc</guid>
      <description>&lt;p&gt;If you are running a Rails application that is more than a few years old, there is a very high probability that you are using &lt;code&gt;ActiveModel::Serializers&lt;/code&gt; (AMS).&lt;/p&gt;

&lt;p&gt;You also probably know that AMS is "dead". The repository has been archived, maintainers have moved on, and 0.10.x is effectively the final version.&lt;/p&gt;

&lt;p&gt;Yet, despite this well-known fact, AMS remains one of the most deployed gems in the ecosystem. Why? Because migration is terrifying. Serialization is the final mile of your API; getting it wrong breaks mobile apps, frontends, and third-party integrations. Furthermore, AMS, specifically when configured with caching, is surprisingly fast. It’s hard to justify a rewrite when the "dead" code is still serving requests in sub-millisecond time.&lt;/p&gt;

&lt;p&gt;We recently faced this dilemma. Our AMS implementation was working, but it was a dead end. We wanted to move to something modern, maintained, and standards-compliant, but we couldn't afford to regress on performance.&lt;/p&gt;

&lt;p&gt;Here is the story of how we pitted the incumbents against the challengers and why we ultimately chose &lt;strong&gt;Alba&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contenders
&lt;/h2&gt;

&lt;p&gt;We narrowed our search to three primary options:&lt;/p&gt;

&lt;h3&gt;
  
  
  ActiveModel::Serializers (The Incumbent)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Deeply integrated, supports view caching, "it just works."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Unmaintained, memory-heavy, implicit behavior ("magic").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants Tested:&lt;/strong&gt; We tested both "Standard" (instantiating new serializers per request) and "Cached" (fetching pre-computed JSON strings from Memcached).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Blueprinter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Declarative, widely used, good documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Syntax is slightly different from the "Rails way," performance is good but generally trails Alba.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alba
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Modern, explicitly designed for speed, highly compatible with the Oj JSON library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Newer ecosystem, requires strict definition of resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Benchmark: Real World Data, Not "Hello World"
&lt;/h2&gt;

&lt;p&gt;Micro-benchmarks are often misleading. Serializing a simple User object with a name and email is easy for any library.&lt;/p&gt;

&lt;p&gt;To make an informed decision, we created a benchmark representing our heaviest real-world scenario: a full E-commerce Order.&lt;/p&gt;

&lt;p&gt;Our test setup involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 Order&lt;/li&gt;
&lt;li&gt;100 Fulfillment Groups (shipments)&lt;/li&gt;
&lt;li&gt;100 Line Items (spread across groups)&lt;/li&gt;
&lt;li&gt;Associated Data: Adjustments (taxes/promotions), Payments, Customers, and Addresses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a heavy payload. It heavily exercises the serializer's ability to handle associations (&lt;code&gt;has_many&lt;/code&gt;, &lt;code&gt;belongs_to&lt;/code&gt;) and nested logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;We implemented the exact same JSON structure across all three libraries.&lt;/p&gt;

&lt;p&gt;The Alba Implementation: Notice how Alba feels familiar to AMS users but with more explicit control.&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;OrderAlbaSerializer&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Alba&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Alba&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Serializer&lt;/span&gt;

  &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;
  &lt;span class="c1"&gt;# Relationships are explicit&lt;/span&gt;
  &lt;span class="n"&gt;many&lt;/span&gt; &lt;span class="ss"&gt;:fulfillment_groups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;resource: &lt;/span&gt;&lt;span class="no"&gt;FulfillmentGroupAlbaSerializer&lt;/span&gt;
  &lt;span class="n"&gt;many&lt;/span&gt; &lt;span class="ss"&gt;:line_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;resource: &lt;/span&gt;&lt;span class="no"&gt;LineItemAlbaSerializer&lt;/span&gt;
  &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="ss"&gt;:venue&lt;/span&gt;

  &lt;span class="c1"&gt;# Computed attributes use a clean block syntax&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:seat_description&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;order&lt;/span&gt;&lt;span class="o"&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;We ran the benchmarks using &lt;code&gt;benchmark-ips&lt;/code&gt; (iterations per second). The results were illuminating.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: Relative performance figures based on our internal testing)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMS (Cached):&lt;/strong&gt; ~3,500 i/s 🏆&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alba (with Oj):&lt;/strong&gt; ~2,800 i/s 🥈&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blueprinter:&lt;/strong&gt; ~2,100 i/s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AMS (Uncached):&lt;/strong&gt; ~600 i/s 🐢&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The "Caching" Elephant in the Room
&lt;/h3&gt;

&lt;p&gt;The most striking data point is that AMS with Caching was the fastest.&lt;/p&gt;

&lt;p&gt;This explains why so many apps are stuck on AMS. When you cache the entire JSON string of a serializer, you are effectively bypassing the serialization work entirely on subsequent hits. It’s hard to beat "fetching a string from RAM."&lt;/p&gt;

&lt;p&gt;However, Alba (backed by the Oj gem) came dangerously close, without caching.&lt;/p&gt;

&lt;p&gt;Alba + Oj was nearly 4-5x faster than standard, uncached AMS. It was so fast that it fundamentally changed our engineering calculus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision: Why We Chose Alba
&lt;/h2&gt;

&lt;p&gt;If AMS Cached is faster, why switch?&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Speed Without Complexity
&lt;/h3&gt;

&lt;p&gt;Caching is hard. Cache invalidation is one of the two hardest problems in computer science (along with naming things and off-by-one errors).&lt;/p&gt;

&lt;p&gt;With AMS, we relied on caching to get acceptable performance. If the cache missed, the user felt it (dropping to 600 i/s).&lt;/p&gt;

&lt;p&gt;With Alba, the baseline performance is exceptional. We get ~2,800 i/s every single time. We don't need to manage complex cache keys. We can simplify our architecture by removing the view caching layer entirely for most endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Oj Factor
&lt;/h3&gt;

&lt;p&gt;Alba is designed to leverage Oj (Optimized JSON), a C-extension for Ruby that is incredibly fast. By setting:&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;# config/initializers/alba.rb&lt;/span&gt;
&lt;span class="no"&gt;Alba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:oj&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alba bypasses much of the Ruby object allocation overhead that slows down other serializers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Safety and Maintenance
&lt;/h3&gt;

&lt;p&gt;AMS 0.10.x is a ghostly dependency. It might break with the next major Rails upgrade. Alba is active, standard-compliant, and its codebase is easy to read and understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Caching Gap (and How We Fixed It)
&lt;/h2&gt;

&lt;p&gt;There was one catch: Alba does not support caching out of the box.&lt;/p&gt;

&lt;p&gt;This is by design. The creators of Alba (and indeed, many in the API community) believe that serialization logic should remain pure, and caching should be handled at the HTTP layer or application boundary. (See &lt;a href="https://github.com/rails/rails/issues/41784" rel="noopener noreferrer"&gt;Rails Issue #41784&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For us, however, fragment caching is critical. We have complex objects (like venues with thousands of seats) where re-serializing everything, even with Alba's speed, is wasteful.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: A Lightweight Concern
&lt;/h3&gt;

&lt;p&gt;Since Alba is just Ruby, adding caching back in was surprisingly trivial. We didn't need the heavy "Cache Adapter" machinery of AMS. We just needed a decorator.&lt;/p&gt;

&lt;p&gt;We wrote a simple &lt;code&gt;AlbaCaching&lt;/code&gt; concern that wraps the &lt;code&gt;serialize&lt;/code&gt; method:&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;module&lt;/span&gt; &lt;span class="nn"&gt;AlbaCaching&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;class_methods&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
      &lt;span class="vi"&gt;@cache_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&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;cache_options&lt;/span&gt;
      &lt;span class="vi"&gt;@cache_options&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;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;root_key: &lt;/span&gt;&lt;span class="kp"&gt;nil&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="k"&gt;unless&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;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_options&lt;/span&gt;
    &lt;span class="c1"&gt;# Construct a cache key from the object and serializer name&lt;/span&gt;
    &lt;span class="n"&gt;key_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_key&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;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&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="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&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;object&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="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&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;key_base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&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;class&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="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&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;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:expires_in&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;hour&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;super&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;Now, we can just mix this into any serializer that needs 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;HeavyVenueSerializer&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Alba&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;AlbaCaching&lt;/span&gt;

  &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

  &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:capacity&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 gave us the best of both worlds: the raw speed of Alba/Oj for 90% of our requests, and the ability to selectively cache heavy fragments where needed, without the bloat of AMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the Migration
&lt;/h2&gt;

&lt;p&gt;With the decision made and the caching gap bridged, we faced one final hurdle: the sheer volume of code. We had dozens of serializers to convert.&lt;/p&gt;

&lt;p&gt;Rewriting them by hand was too much work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: If you are on Ruby 3.1+, you should check out the excellent &lt;a href="https://github.com/okuramasafumi/alba_migration" rel="noopener noreferrer"&gt;alba_migration&lt;/a&gt; gem, which handles much of this automatically. Since our application was still running on an older Ruby version, we had to roll our own solution.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We wrote a simple "bootstrapping" script (&lt;code&gt;bin/alba_bootstrap.rb&lt;/code&gt;) to handle the syntax-swapping drudgery.&lt;/p&gt;

&lt;p&gt;It wasn't an AST parser or a complex transpiler. It was just simple Ruby string manipulation:&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;# bin/alba_bootstrap.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# 1. Swap Inheritance for Include&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt; ActiveModel::Serializer/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/class (.*?)(\n|$)/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"class &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  include Alba::Resource&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Convert Associations&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/has_many/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'many'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/has_one/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'one'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. Prompt for Manual Logic Review&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/def (.*?)\n(.*?)\n  end/m&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;match&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="s2"&gt;"# TODO: Convert method '&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' to attribute block&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'# '&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="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;This script didn't produce perfect code, but it got us 80% of the way there. It handled the boilerplate, leaving us to focus on the interesting parts: converting complex custom methods into Alba's clean attribute blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Leaving &lt;code&gt;ActiveModel::Serializers&lt;/code&gt; feels like leaving an old apartment. It had its quirks, but it was home.&lt;/p&gt;

&lt;p&gt;However, the move to Alba has been a breath of fresh air. We traded a complex, unmaintained caching strategy for raw, highly optimized serialization throughput. The code is cleaner, the benchmarks are solid, and we sleep better knowing our API layer is future-proof.&lt;/p&gt;

&lt;p&gt;If you are still holding onto AMS because "it works," I highly recommend creating a benchmark with your heaviest model and giving Alba a spin. You might find that you don't need that cache as much as you thought.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>performance</category>
      <category>serialization</category>
      <category>alba</category>
    </item>
    <item>
      <title>Rails Removed Email Obfuscation. Here's How I Brought It Back with Stimulus</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Thu, 08 Jan 2026 14:10:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/rails-removed-email-obfuscation-heres-how-i-brought-it-back-with-stimulus-4b1a</link>
      <guid>https://dev.to/syedaslam/rails-removed-email-obfuscation-heres-how-i-brought-it-back-with-stimulus-4b1a</guid>
      <description>&lt;p&gt;I recently needed to add a simple contact email to my footer. It sounds trivial: just use a &lt;code&gt;mailto:&lt;/code&gt; link, right? But in 2024, putting a raw email address in your public HTML is basically an invitation for spam.&lt;/p&gt;

&lt;p&gt;I remembered that Rails used to handle this for us. The &lt;code&gt;mail_to&lt;/code&gt; helper had a handy &lt;code&gt;encode: "hex"&lt;/code&gt; option that would scramble the email address to confuse harvesters. I went to the docs, ready to use it, only to find it was removed in Rails 4.0. The suggested alternative? "Install a gem."&lt;/p&gt;

&lt;p&gt;I didn't want to add a dependency just to render one link. And frankly, the old JavaScript-based obfuscation techniques (like &lt;code&gt;document.write&lt;/code&gt; or &lt;code&gt;eval&lt;/code&gt;) are now blocked by modern Content Security Policies (CSP).&lt;/p&gt;

&lt;p&gt;So I decided to build a modern solution. I wanted the best of both worlds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Zero Spam:&lt;/strong&gt; The email address should effectively not exist in the DOM.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Great UX:&lt;/strong&gt; To the user, it should behave exactly like a normal link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how I solved it using Stimulus.&lt;/p&gt;

&lt;p&gt;The goal became simple: The email address should not exist in the DOM until the user actually asks for it.&lt;/p&gt;

&lt;p&gt;If a bot scans the source code, it should see nothing useful. If a human hovers over the icon, it should work instantly.&lt;/p&gt;

&lt;p&gt;This is a perfect job for Stimulus.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Approach
&lt;/h3&gt;

&lt;p&gt;Instead of writing the email into the href, we split it up and hide it in data attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"mail-obfuscator"&lt;/span&gt;
  &lt;span class="na"&gt;data-mail-obfuscator-user-value=&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;
  &lt;span class="na"&gt;data-mail-obfuscator-domain-value=&lt;/span&gt;&lt;span class="s"&gt;"ayatflow.com"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Icon --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To a bot, this is a link to nowhere (&lt;code&gt;#&lt;/code&gt;). It contains the strings "hello" and "ayatflow.com", but without the &lt;code&gt;@&lt;/code&gt; or the &lt;code&gt;mailto:&lt;/code&gt; context, it's just noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Magic (Stimulus Controller)
&lt;/h3&gt;

&lt;p&gt;We need a controller that assembles these pieces only when the user interacts with the link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/mail_obfuscator_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ensure it's dead on arrival&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reveal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Assemble only when needed&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`mailto:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userValue&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domainValue&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="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cover your tracks&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Ruby Helper
&lt;/h3&gt;

&lt;p&gt;To make this reusable, I wrapped it in a helper that feels just like the old &lt;code&gt;mail_to&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="c1"&gt;# app/helpers/application_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;obfuscated_mail_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&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;domain&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;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"mail-obfuscator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;mail_obfuscator_user_value: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;mail_obfuscator_domain_value: &lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# The secret sauce: Trigger on everything&lt;/span&gt;
    &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"mouseover-&amp;gt;mail-obfuscator#reveal focus-&amp;gt;mail-obfuscator#reveal mouseout-&amp;gt;mail-obfuscator#reset blur-&amp;gt;mail-obfuscator#reset click-&amp;gt;mail-obfuscator#reveal"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:href&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"#"&lt;/span&gt;

  &lt;span class="n"&gt;link_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:href&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&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;h3&gt;
  
  
  The "Reset" Trick
&lt;/h3&gt;

&lt;p&gt;You might notice the &lt;code&gt;reset()&lt;/code&gt; action mapped to &lt;code&gt;mouseout&lt;/code&gt; and &lt;code&gt;blur&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is my favorite part. As soon as your mouse leaves the link, it reverts back to &lt;code&gt;href="#"&lt;/code&gt;. If you try to "Right Click -&amp;gt; Copy Link Address" without hovering directly (or if you move your mouse away too fast), you get nothing.&lt;/p&gt;

&lt;p&gt;It makes the window of opportunity for scraping extremely small, without impacting the UX for a genuine user who just wants to say hello.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Sometimes the best solutions aren't about complex encryption, but just being a little bit slippery. This approach is lightweight, CSP-compliant, and keeps my inbox just a little bit cleaner.&lt;/p&gt;

&lt;p&gt;Code is available in this &lt;a href="https://gist.github.com/aslam/2c682b265dafa3e5dc93488d7f9ce1ac" rel="noopener noreferrer"&gt;Gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
      <category>security</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Formatting ActiveSupport::Duration Objects in Rails</title>
      <dc:creator>Syed Aslam</dc:creator>
      <pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/syedaslam/formatting-activesupportduration-objects-in-rails-2521</link>
      <guid>https://dev.to/syedaslam/formatting-activesupportduration-objects-in-rails-2521</guid>
      <description>&lt;p&gt;Rails’ &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/Duration.html" rel="noopener noreferrer"&gt;&lt;code&gt;ActiveSupport::Duration&lt;/code&gt;&lt;/a&gt; objects don’t include a straightforward way to output clean, human-readable strings. By default, you’re left with &lt;a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/duration.rb#L444" rel="noopener noreferrer"&gt;&lt;code&gt;#inspect&lt;/code&gt;&lt;/a&gt;, which is limited and lost locale support after Rails 5.1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fox2qnvs4dgvcd511vgt9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fox2qnvs4dgvcd511vgt9.png" alt="Duration" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Manual Helper
&lt;/h2&gt;

&lt;p&gt;You could roll your own helper in a view:&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;duration_as_sentence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parts&lt;/span&gt;
  &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:minutes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;days:    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;one: :d&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="ss"&gt;hours:   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;one: :h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;other: :hrs&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="ss"&gt;minutes: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;one: :m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;other: :mins&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="nf"&gt;sort_by&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&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;unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&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;val&lt;/span&gt;&lt;span class="si"&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;val&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="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:one&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:other&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="nf"&gt;to_sentence&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 works, but it’s limited, not very configurable, and ignores localization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing duration_in_words
&lt;/h2&gt;

&lt;p&gt;To address this, I built a small gem called &lt;a href="https://github.com/aslam/duration_in_words" rel="noopener noreferrer"&gt;duration_in_words&lt;/a&gt;.&lt;br&gt;
It provides a view helper that formats ActiveSupport::Duration objects into concise, localized strings like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;include ActionView::Helpers::DurationHelper

&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; duration &lt;span class="o"&gt;=&lt;/span&gt; 2.hours
&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; duration_in_words&lt;span class="o"&gt;(&lt;/span&gt;duration&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"2h"&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; duration &lt;span class="o"&gt;=&lt;/span&gt; 1.day + 2.hours + 30.minutes
&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; duration_in_words&lt;span class="o"&gt;(&lt;/span&gt;duration&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"1d 2h and 30m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Formatting Options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;:format&lt;/code&gt; option&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two formats are supported: :compact (default) and :full.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;in_words&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;compact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}Std."&lt;/span&gt;
            &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}Std."&lt;/span&gt;
          &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}Min"&lt;/span&gt;
            &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}Min"&lt;/span&gt;
          &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}s"&lt;/span&gt;
            &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%{count}s"&lt;/span&gt;
          &lt;span class="na"&gt;support&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;words_connector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
            &lt;span class="na"&gt;two_words_connector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;und&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
            &lt;span class="na"&gt;last_word_connector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;und&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&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;duration&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;day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

&lt;span class="n"&gt;duration_in_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locale: :de&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "1T 2Std. und 30s"&lt;/span&gt;

&lt;span class="n"&gt;duration_in_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: :full&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locale: :de&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "1 Tag, 2 Std., und 30 Sekunden"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;When you need to report durations in a user-friendly way, &lt;code&gt;duration_in_words&lt;/code&gt; provides a clean, configurable, and locale-aware solution. It’s a big improvement over the default &lt;code&gt;#inspect&lt;/code&gt; output and saves you from reinventing formatting helpers.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>activesupport</category>
      <category>helpers</category>
    </item>
  </channel>
</rss>
