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