<?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: Braden King</title>
    <description>The latest articles on DEV Community by Braden King (@brazenbraden).</description>
    <link>https://dev.to/brazenbraden</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%2F3432716%2Fc6a05f6d-6d99-4a1a-ab13-e0cf2dbcaddf.jpeg</url>
      <title>DEV Community: Braden King</title>
      <link>https://dev.to/brazenbraden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brazenbraden"/>
    <language>en</language>
    <item>
      <title>Why I'm Still Using jQuery in 2025 (Never gonna give you up)</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Fri, 07 Nov 2025 02:33:47 +0000</pubDate>
      <link>https://dev.to/brazenbraden/why-im-still-using-jquery-in-2025-never-gonna-give-you-up-3j91</link>
      <guid>https://dev.to/brazenbraden/why-im-still-using-jquery-in-2025-never-gonna-give-you-up-3j91</guid>
      <description>&lt;p&gt;While grinding through the monumental JuggleBee upgrade, one of the recurring debates was: what do we modernize, and what do we leave the heck alone? Plenty got a shiny new coat of paint (see my earlier posts for all the juicy details), but one area I deliberately left untouched was the frontend JavaScript. These days, jQuery is seen as a relic of a bygone era — a noble steed well past its sell-by date. Hotwire now ships as the Rails 8 default, and heavyweight contenders like React and Vue have taken over the frontend spotlight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Kept It
&lt;/h2&gt;

&lt;p&gt;So why didn’t I replace it with Hotwire or Stimulus when doing the big Rails 8 overhaul?&lt;/p&gt;

&lt;p&gt;Because honestly... it still &lt;em&gt;slaps&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Sure, jQuery has fallen out of favour, and yes, I could’ve followed the modern path. But here’s the reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It still works. Flawlessly.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The architecture is solid.&lt;/strong&gt; It keeps behavior modular and scoped. No random scripts firing across unrelated pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It’s expressive and readable.&lt;/strong&gt; You can look at the markup and know exactly which components will be applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewriting it would’ve cost me weeks&lt;/strong&gt; — and yielded very little gain for the effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s easy to chase shiny new frameworks. But this setup? It’s already solved the very problems frameworks like Stimulus are trying to address — just in our own way, before they existed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Technical debt isn't defined by age. It's defined by pain."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This jQuery-based framework causes me &lt;strong&gt;zero&lt;/strong&gt; pain. It’s clean, consistent, and extendable. And I’m not paying a complexity tax for keeping it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Beautiful Framework
&lt;/h2&gt;

&lt;p&gt;Back in the yonder years, when development on JuggleBee first commenced, jQuery was king. "Write less, do more" as the slogan proudly declared. These were the days before Turbo, pre-SPAs, when frameworks like React were still internal tools being tinkered on at Facebook. Even then, I liked my code clean, purposeful, and targeted. Every function should do its job — and only its job — exactly where it’s meant to. The typical jQuery spaghetti of the time grated on me.&lt;/p&gt;

&lt;p&gt;Take this scenario: I’ve got a lovely &lt;code&gt;Select2&lt;/code&gt; dropdown on the listings page to filter by category. All good. But why is that whole library still being pulled in and initialized on the About Us page, which has no dropdowns in sight? This was a symptom of how jQuery scripts were commonly written — everything ran everywhere. I wanted modularity, not mayhem. And so, the “Behaviour Framework” was born.&lt;/p&gt;

&lt;p&gt;So what exactly did we build?&lt;/p&gt;

&lt;p&gt;In essence, it's a lightweight, declarative behaviour system — think of it like a primitive version of Stimulus or Alpine. Components declare themselves in the markup, and a central engine takes care of the wiring when the DOM loads. This means zero manual JS in our views, and zero unnecessary script execution across unrelated pages.&lt;/p&gt;

&lt;p&gt;Let me show you how it works...&lt;/p&gt;

&lt;p&gt;At the heart of it all is the &lt;code&gt;Behaviour&lt;/code&gt; class. Its role is simple but powerful: register UI components and apply them only when needed — and only on the elements that require them. Here's a high-level look at how it works under the hood:&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;// Note: classes didnt exist so the "Class" we're calling below is a custom implementation&lt;/span&gt;
&lt;span class="nx"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Behaviour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="na"&gt;addFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instantiator&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;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instantiator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;addFilters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;filters&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="nf"&gt;addFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filter&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;span class="na"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&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="nf"&gt;_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proxy&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;_apply&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;unapply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&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="nf"&gt;_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;behaviourClassName&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;behaviourResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getBehaviourResult&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;behaviourClassName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;behaviourResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deinit&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="nx"&gt;behaviourResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deinit&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;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// rest of the implementation ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows us to define small, focused components — like a date picker — in total isolation. No global bleed. No script soup. Just self-contained behaviour.&lt;/p&gt;

&lt;p&gt;One of the earliest components we wrote using this system was actually said date picker. At the time, we were using Bootstrap and jQuery UI plugins — each with their own quirks. So we wrapped them up into their own isolated modules, making them easier to configure and reuse across different forms, pages, and even admin views.&lt;/p&gt;

&lt;p&gt;Here’s what the class for that looked like:&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="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DatePicker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;04/07/2015&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dd/mm/yyyy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;todayHighlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;weekStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;function&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;options&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="nf"&gt;setOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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="nf"&gt;datepicker&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;options&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;p&gt;Once defined, we simply register our component with the behaviour system so it knows how to wire it up when the time comes:&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;behaviour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Behaviour&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;behaviour&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFilters&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Component.DatePicker&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="na"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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;options&lt;/span&gt;&lt;span class="p"&gt;)&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;new&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DatePicker&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;options&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;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;behaviour&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;p&gt;Now comes the beauty of it — in your HAML or HTML, you declare the behaviour directly in your markup using a &lt;code&gt;data-behaviour&lt;/code&gt; attribute. When the page loads, only the relevant components are instantiated, and only for the elements that need them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haml"&gt;&lt;code&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;as: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s1"&gt;'Start Date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"e.g. &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;example&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="ss"&gt;input_html: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="s1"&gt;'%d/%m/%Y'&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="ss"&gt;behaviour: &lt;/span&gt;&lt;span class="s1"&gt;'Component.DatePicker'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'options-component.datepicker'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"startDate" : "'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'"}'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, our behaviours are neatly attached, scoped, and easy to manage.&lt;/p&gt;

&lt;p&gt;It’s clean. It’s obvious. And it means onboarding a new team member doesn’t require a 2-hour walkthrough of where the JavaScript magic lives — it’s all in the markup and component files.&lt;/p&gt;

&lt;p&gt;This approach meant zero wasted cycles, faster page loads, and a far more maintainable codebase. Behaviourally-aware pages, declarative setup, and modular control over every UI component. In today’s parlance, it’s not far off what Stimulus or Vue’s component system offers — just built years earlier and tailored to our exact needs.&lt;/p&gt;

&lt;p&gt;This is the short version — enough to give you a taste of how it all works. In a follow-up post, I’ll take a deep dive into the full implementation, including sibling systems like the &lt;code&gt;EventBus&lt;/code&gt;, &lt;code&gt;Trigger&lt;/code&gt; logic, Adapters, and more.&lt;/p&gt;

&lt;p&gt;Credit where it’s due: this wasn’t a solo effort. The framework was shaped with the brilliant Paul Schwarz, and together we built something that — dare I say — still holds up surprisingly well in 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  jQuery Isn’t Dead. It’s Just Retired.
&lt;/h2&gt;

&lt;p&gt;Do I recommend jQuery for new apps in 2025? Of course not.&lt;br&gt;
But for apps that already have it — and especially those with a system like this layered on top — there's no real reason to tear it out just to chase trends.&lt;/p&gt;

&lt;p&gt;This is the kind of code you keep &lt;em&gt;because&lt;/em&gt; it works — not in spite of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Up Next
&lt;/h2&gt;

&lt;p&gt;In Part 2, I’ll take you on a deep dive into the guts of the Behaviour framework: how components are hydrated, how the &lt;code&gt;EventBus&lt;/code&gt; enables decoupled comms between modules, how &lt;code&gt;Trigger&lt;/code&gt; handles custom DOM events, and more.&lt;/p&gt;

&lt;p&gt;It’s a little old-school, sure — but it's also still slick as hell.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>jquery</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Release Process</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Thu, 23 Oct 2025 23:42:44 +0000</pubDate>
      <link>https://dev.to/brazenbraden/release-process-3d2g</link>
      <guid>https://dev.to/brazenbraden/release-process-3d2g</guid>
      <description>&lt;p&gt;After having followed the various conventions defined by our &lt;a href="https://brazenbraden.com/posts/git_conventions/" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and &lt;a href="https://brazenbraden.com/posts/github_process/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; processes, we should now be in a good place to take the code that has been produced, get it ready for QA, merge and release it. Every organisation will have their processes and workflows set around this, some good, some bad. I worked with my team to build a robust and practical release process which best suited our needs at the time, allowing for a quick development - QA - release lifecycle. We didn't quite achieve the continuous-release dream, but we got close.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: This is an evolving document which has been altered and tuned over the years. It may not apply to you and your situation but perhaps there is a nugget or two here that could improve your workflow and developer happiness.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  QA / Testing
&lt;/h2&gt;

&lt;p&gt;As anyone will know, the QA process of manually testing and sanity-checking code changes in a piece of work can often take 10 times the duration it took to make the changes in the first place, depending on the size of the changes requiring testing. No one wants to hand over some work for QA, and have the QA engineer spend a day testing it, only to find a silly bug or edge case which could have been caught by a code review or proper test harness, resulting in the PR being handed back to the developer to make the fixes, resetting any QA progress. QA time is limited and not to be wasted. Therefore, we do as much as we can before handing over to QA, ensuring to the best of our ability, that they have a bug-free, fully functional code change to test, limiting pushback.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Green CircleCI workflow
&lt;/h3&gt;

&lt;p&gt;We were in a good place with our test coverage, with a minimum of 98% coverage. Our testing suite was split up over a parallelised CircleCI workflow allowing for relatively fast builds and giving us the chance to ensure our entire test suite went green before moving on to requesting developer reviews. 95% of the time, our test harness would catch any accidental bugs or code smells, enabling us to pass clean, "bug-free" code to the team to review.&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%2Fr454dq4haut2vwguxqrq.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%2Fr454dq4haut2vwguxqrq.png" alt="Our CircleCI Workflow" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For this post, I didn't bother waiting for the entire build process to go green before screenshotting, but you get the idea&lt;/em&gt;.*&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deploy to an integration environment
&lt;/h3&gt;

&lt;p&gt;Thanks to our DevOps engineer, we had a flexible integration deployment system built right into CircleCI. On approval of integration deployment, a fresh new VM would be spun up, our code checked out, and all systems set up for manual testing. We would sanity check our changes on a "production" system because all too often, you hear the phrase "Well, it worked on my machine". Subtle differences between development and production environments can result in a working dev build failing in production. This also gives us a chance to properly document how QA should go about testing our changes for when it gets handed over. For much larger features, we would use these environments for demos and to demonstrate the new functionality to the rest of the team, assisting with their code reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Developer code reviews
&lt;/h3&gt;

&lt;p&gt;Once we were confident all tests and code quality checks were passing, and we have sanity checked our own work, we would then request two developer reviews. This gave other developers the chance to familiaralize themselves with the changes, and ensure the code was clean and well written, fully tested and optimised. Oftentimes, a fresh set of eyes might identify an edge case that was missed or realise that a query is N+1 and could be optimised. Be there required code changes or not, after all is resolved and the team is happy, they would provide their two green ticks on the PR. We would also ensure our branch commits are in a good state by having done an interactive rebase against &lt;code&gt;master&lt;/code&gt;, squashing and/or renaming commits to follow our conventions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hand over to QA
&lt;/h3&gt;

&lt;p&gt;Once we are confident that our code works, we have the approval of the team and we are happy that our manual testing and sanity checks have all passed, the PR finally gets flagged for QA. QA will then test our code on the integration environment we used for our testing (creating their bespoke test data) and be guided by our specified testing steps and notes. A QA intends to break the code, so it is at this point all the weird and missed edge case testing usually happens and bugs are discovered, if present. Regardless, once QA has taken their sledgehammer to the work, and finds it without fault, they will provide their final green tick on the PR, leaving the PR with a total of 3 green ticks. We are now in a state to merge the work into &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging
&lt;/h2&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%2Fzb6avriohcownr3izwn1.gif" 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%2Fzb6avriohcownr3izwn1.gif" alt="merging" width="400" height="224"&gt;&lt;/a&gt;&lt;br&gt;
Before we merge a branch to &lt;code&gt;master&lt;/code&gt;, a couple of final steps are taken to ensure as smooth a process as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Rebase and commit squashing
&lt;/h3&gt;

&lt;p&gt;During the previous QA testing phase, there may have been various code changes required or bugs resolved. Before we commit to merging the branch after all the checks are green, we ensure our branch is up-to-date with the latest &lt;code&gt;master&lt;/code&gt; by performing an interactive rebase against it, and squash any necessary commits, ensuring a clean, flat, and well-written git history is maintained (as per our &lt;a href="https://brazenbraden.com/posts/git_conventions/" rel="noopener noreferrer"&gt;Git Conventions&lt;/a&gt;). Ideally, there would have been no additional code modifications, so this step could be skipped as we do this before handing it over for dev review.&lt;/p&gt;

&lt;p&gt;If there were merge conflicts that needed resolving, that means other work has made it in which may affect your code changes. In this instance, we would, based on the conflicts, possibly ask QA to go over the PR one more time before it gets merged, based off team decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Checking for migrations
&lt;/h3&gt;

&lt;p&gt;Migrations can be tricky things. Testing a migration against a local database means nothing when run against a production database. Something as simple as changing a column name can bring an application to its knees if done incorrectly. Very often, one will see code-altering data in the database inside the migration (yes, this is a bad practice and shouldn't happen in a perfect world, but this is not a perfect world) which, when operating on millions of rows in a production database, can cause database locks and timeouts, failing the deployment process. Having been bitten too many times in the past by dodgy migrations requiring a release rollback, we opted to isolate any PRs that contained any database migration changes and deploy them as standalone. It is at this point that QA would flag the PR for isolated deployment to, in the worst case of a rollback, not have other PRs affected by the rollback.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Merging into &lt;code&gt;master&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;master&lt;/code&gt; branch was protected against merging by everyone except our lead QA engineer, the most senior dev and the CTO. In 99.9% of instances, merging was done by our QA engineer, allowing for a controlled merge process. This also prevented untested code from sneaking into &lt;code&gt;master&lt;/code&gt; under the guise of a "hotfix" or other such shenanigans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release
&lt;/h2&gt;

&lt;p&gt;Our dream was to have rolling deploys - as soon as anything was merged into &lt;code&gt;master&lt;/code&gt;, it would deploy. At the time of writing, however, we were working off manual releases, usually containing a handful of PRs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tag and generate release docs
&lt;/h3&gt;

&lt;p&gt;GitHub made it easy to generate our release notes as it can do so automatically when creating a new release. Specify the new tag to be applied to &lt;code&gt;master&lt;/code&gt; branch (major/minor/micro version bump), ask Github to generate the release notes and that's it. Thanks to our strict use of git commit conventions, the release notes description was clear without needing additional modification most of the time. For larger, more involved features, additional context would be supplied to better communicate the changes with our users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration-induced isolated deployment
&lt;/h3&gt;

&lt;p&gt;If a PR containing a database migration is flagged, a release will be created containing all previous PRs that have been merged since the last release, excluding the migration PR, and that will be deployed. The PR containing the migration would then get merged and a new release would be created straight away with only this PR. This was to ensure that there are no potential issues from compound migrations and also to allow quick and simple rollback should there be an issue. All this was made easier by having the &lt;code&gt;master&lt;/code&gt; branch locked down to lead QA merging only, meaning nothing else could slip from elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Press the button
&lt;/h3&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%2Foxsde8plbbuxus43xyd7.gif" 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%2Foxsde8plbbuxus43xyd7.gif" alt="push the button" width="500" height="281"&gt;&lt;/a&gt;&lt;br&gt;
Once the release tag has been created, the entire CircleCI build would run once more, running the entire test suite, including the slower integration test suite (not run by default) and once green, the release would be approved through CircleCI and off it goes. As we had a blue-green delivery process in place, a whole new cluster of pods would spin up with the new release, and only after all the various checks were completed ensuring all services were running without issue, we would have the load balancer switch over to the new pods and start killing off the old ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's all folks
&lt;/h2&gt;

&lt;p&gt;And that concludes part 3 of my documentation about the processes and workflows we built and fine-tuned over the years around the development lifecycle. It may not be perfect and there was a lot more we wanted to do (rolling deploys, etc) which we hadn't quite gotten to, but what we did have in place worked well for us. Sometimes the smallest change at the beginning of the process can produce significant wins at the end of the process. For instance, how our release note documentation was generated descriptively and concisely based on our git commit conventions, or how our GitHub documentation process helped us find where bugs were created years later due to easy searching.&lt;/p&gt;

&lt;p&gt;All of these processes took some time, effort and accountability to become habits. There would be moans and groans when a PR was rejected because the branch name didn't match the pattern (look up Git move) or when an interactive rebase to squash that final "fix issue" commit tacked on during the review process was insisted upon, or blocking a PR until it had a decent enough description. Some might find these requests to be petty but looking back on the bubbling effect of them, the entire team would agree it was worth putting in that extra leg work at the end of the day.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>development</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>Issue and Pull Request Conventions</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Sun, 12 Oct 2025 09:21:57 +0000</pubDate>
      <link>https://dev.to/brazenbraden/issue-and-pull-request-conventions-372p</link>
      <guid>https://dev.to/brazenbraden/issue-and-pull-request-conventions-372p</guid>
      <description>&lt;p&gt;My previous post on &lt;a href="https://brazenbraden.com/posts/git_conventions/" rel="noopener noreferrer"&gt;GIT Conventions&lt;/a&gt; broke down various conventions around branching and committing code. As part of building a solid process around development, the next area to deal with is that of getting those branches and commits together for a merge and release. This is usually done via some sort of ticket on an issue tracker and code pull request. The following guide was developed as a collaborative effort with my team and lead to a more robust and progressive environment with improved communication and overall general happiness, even though it adds a little extra overhead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: This is an evolving document which has been altered and tuned over the years. It may not apply to you and your situation but perhaps there is a nugget or two here that could improve your workflow and developer happiness.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Documentation
&lt;/h2&gt;

&lt;p&gt;When it comes to software development in general, documentation about the work being performed is incredibly important. Not only for your own understanding but for that of your team (and future you/team). Developers are lazy; documentation is a chore and it just slows us down; we want to code and deploy at the speed of light! The number of times I've had to go hunting through hundreds of previous PRs with incomprehensible titles and missing descriptions in order to find the correct one which holds the change that introduced a bug, is way too high and wastes and inordinate amount of my time and sanity.&lt;/p&gt;

&lt;p&gt;A feature request or support ticket can come in from many different sources, perhaps a verbal suggestion from a product manager, or a ticket from a bug reporting system like Zendesk to JIRA. In order to unify these multiple sources into a single source of truth for the development team, the feature / issue would be written down into a Github Issue (we favoured Github as our central work hub, making use of their great tools), referencing the source when available.&lt;/p&gt;

&lt;p&gt;Once the issue was drafted, it's prioritised and assigned to a developer. The assignee would then spend a little time investigating the issue, collecting additional information, communicating with the various parties involved, and fleshing out the issue in a detailed and descriptive manner. With the issue in a good place, work can begin and once done, a PR created and linked to the issue. The PR didnt need to contain too much information, just a short description to help with searching for the most part, as the issue covers everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues
&lt;/h2&gt;

&lt;p&gt;The central place for all information regarding a piece of work is the &lt;strong&gt;issue/ticket&lt;/strong&gt;. We chose to use Github for our issues and pull requests however the structure defined here works with any project management tool. At the time, we had defined a &lt;code&gt;ISSUE_TEMPLATE&lt;/code&gt; in our &lt;code&gt;.github&lt;/code&gt; repository but that method is now deprecated in favour of the &lt;a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates" rel="noopener noreferrer"&gt;multi-issue type setup&lt;/a&gt;, however, each section still applies.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Description
&lt;/h3&gt;

&lt;p&gt;The entrypoint and most important part of the issue. When developing a new feature, detail what the feature is, how it is intended to work, what problem will it solve and what was the reasoning behind this feature request in the first place. If the issue is a bug fix, detail what the bug is, how it was found and what was the cause of it, following with how you went about resolving it. This may sound like you need to write a thesis to explain the code change but all of this could easily be construed in a few concise sentences. It can take a little practice and if you're struggling to form a short but clear, comprehensive description of the task at hand, perhaps ask ChatGPT to summarise it for you :P&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Links
&lt;/h3&gt;

&lt;p&gt;This section contains all links associated with the changes needed so that those inspecting the issue can quickly and easily navigate to all related material. This could be other PRs which are linked to the change (perhaps backend and frontend PRs are necessary), the support tickets that spurred on the change, documentation or external resources (blog post, stackoverflow entry, etc) used in order to achieve the solution and whatever else could be of interest.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Integration Environment
&lt;/h3&gt;

&lt;p&gt;At my previous employ, our CI/CD pipeline would deploy the PR to a newly spun up integration environment with seeded database in order for QA to have a running instance to test on. In the nature of providing all information in one place, this section included the URL to the CI/CD process responsible for creating (and destroying) the temporary environment, all associated web addresses to the various platforms and servers that might need accessing, as well as SSH instructions to the integration server in questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Testing Notes
&lt;/h3&gt;

&lt;p&gt;This section allows you to define a structured and clear process for QA to test the changes you have made. Perhaps they are same as the reproduction steps cited in a bug report, or detail how to use a new piece of functionality. A step-by-step guide will help QA test your change but also identify where they could attempt to break it, ensuring you have put in place proper protection mechanisms such as validations or rescue conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Migrations (Optional)
&lt;/h3&gt;

&lt;p&gt;Database migrations can be a real headache if not done correctly. Sometimes a migration file will be added but the schema forgotten. Or the migration itself contains code to update a table with some new / altered values which falls apart on massive data sets. You may be able to correct mad migrations locally by just dropping and recreating the database, but you will have no such luck in production. Failed migrations can really throw a spanner in the works, therefore it is most important to make your team mates / QA / product aware that your work includes migrations and the team as a whole can battle test it (perhaps with a dry-run against a production replica database) and be on hand should anything go wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Additional Notes (Optional)
&lt;/h3&gt;

&lt;p&gt;Sometimes, there are other random tidbits you wish to include, perhaps direct messages to team members regarding anything written above, or notes to yourself reminding you to deal with something you stumbled upon when doing this work. This is where you can dump all those extra thoughts / notes / ideas / suggestions so that you can have it off your mind and have it available to remind your later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;As I am unable to share actual issues from work (trade secrets and all that), I have put together a dummy example based on the information above. The description could be a bit more detailed, however, there was image size considerations to take.&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%2F58vbgfb6jxde9ydv4ty8.jpg" 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%2F58vbgfb6jxde9ydv4ty8.jpg" alt="Example Github Issue" width="800" height="764"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Request (PR)
&lt;/h2&gt;

&lt;p&gt;Given we provided all the information needed for the work done in the associated Issue, as well as ensuring all discussion is kept to the issue (comments), we keep the PR relatively simple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A basic summary of the work done (one or two sentences) was required, purely in order to assist searching for the PR in the future (an empty PR description gets lost forever).&lt;/li&gt;
&lt;li&gt;After the short summary, a link to the Issue is added. If you use Github projects, you can conveniently create links between PRs and Issues, more in &lt;a href="https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue" rel="noopener noreferrer"&gt;their documentation here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The owner(s) be assigned to the PR.&lt;/li&gt;
&lt;li&gt;Link other relevant PRs to connect GitHub timelines&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Up
&lt;/h2&gt;

&lt;p&gt;As mentioned up at the top, by nature, developers are lazy. There could be push-back when attempting to introduce these conventions which is understandable but in time, the team will come to understand and appreciate the detail and extra depth provided around any piece of work, especially when it comes to bug hunting, refactoring or feature improvement. Being able to sit and clearly write down what you are doing, why you are doing and how you are doing also allows us to step back from the code and look at it from a wider perspective, which in turn could open our eyes to other possible solutions, questions, or problems which could arise from the planned work.&lt;/p&gt;

&lt;p&gt;The next post (and final in this series) will focus on turning our completed work with detailed issue and associated PR, into a mergable unit which undergoes QA, merging and finally release!&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>Git Conventions</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Fri, 26 Sep 2025 14:41:25 +0000</pubDate>
      <link>https://dev.to/brazenbraden/git-conventions-kcj</link>
      <guid>https://dev.to/brazenbraden/git-conventions-kcj</guid>
      <description>&lt;p&gt;Over the last decade or so, I have had the opportunity to experiment with various git usage strategies and styles. Ranging from branch names, to pull request descriptions, a lot of time has been spent thinking about how to best describe our work to convey as much information clearly and concisely to others. There are a wealth of strategies or style guides on how to best utilise the git ecosystem but there is no silver bullet that solves all potential scenarios. What follows is an outline of some of the rules and guides I have incorporated into my daily git life which has best worked for my team and me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: This is an evolving document which has been altered and tuned over the years. It may not apply to you and your situation but perhaps there is a nugget or two here that could improve your workflow and developer happiness.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Branches
&lt;/h2&gt;

&lt;p&gt;99% of the time, code changes fall into one of a handful of categories/types. When creating a new branch, naming it something clear and meaningful can help other developers know at a glance what sort of changes are been introduced. It can also simplify finding a branch using tab completion locally when jumping between branches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch Types
&lt;/h3&gt;

&lt;p&gt;The following branch &lt;strong&gt;types&lt;/strong&gt; should suit most use cases. These can be refined or altered depending on your use case (for instance, one might not utilise the &lt;strong&gt;style&lt;/strong&gt; type if they work on CLI scripts for a living)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;feature&lt;/strong&gt; &lt;em&gt;- a new feature&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fix&lt;/strong&gt; &lt;em&gt;- bug fixes, hotfixes, typos, etc&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;style&lt;/strong&gt; &lt;em&gt;- UI / UX style changes&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test&lt;/strong&gt; &lt;em&gt;- adding or refactoring tests; no production code changes&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;housekeeping&lt;/strong&gt; &lt;em&gt;- everything from code refactoring to updating documentation&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;devops&lt;/strong&gt; &lt;em&gt;- updates to configuration files, software versions, etc&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Branch Format
&lt;/h3&gt;

&lt;p&gt;The proposed branch format will make it easy to see at a glance what type of work has been done in the branch with a clean and easy-to-read branch name. The max length of a branch name is 255 characters but where possible, I try to keep the length to no more than 50 characters for readability purposes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type-name_of_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type is separated from the branch name with a &lt;code&gt;-&lt;/code&gt; while the rest of the name is done in snake case (words separated by &lt;code&gt;_&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Alternatively, if using a task/issue tracking application, like JIRA or GitHub projects, it adds additional visibilty to include the issue/ticket number in with the branch, and using a &lt;code&gt;/&lt;/code&gt; instead of &lt;code&gt;-&lt;/code&gt; can aid in making the branch name a little easier to read, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type/ticket#-name_of_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Examples branch names:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature-add_user_authentication
fix-about_page_infinite_redirect

// or

feature/21-add_user_authentication
fix/42-about_page_infinite_redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Commits
&lt;/h2&gt;

&lt;p&gt;Developing a structured format for our commit messages will clarify each commit's purpose resulting in a cleaner git history. At a glance, one will know if a commit is intended to add a feature, fix a bug or just improve code quality. We should aim to keep the work done within a branch small and testable, often resulting in there only being a single commit per branch, however, it's better to have lots of small atomic commits within a branch than having all the commits squashed into one before merging. This makes bisecting introduced bugs a lot easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit Message Template:
&lt;/h3&gt;

&lt;p&gt;This is a sample commit template I have been using for a while now. As I try to keep my commits granular and bespoke, I define the first line leaving everything else commented out. However, if you are pushing up a commit that involves a lot of changes, perhaps references a specific issue or task item, or includes changes that could affect future development, it is recommended to flesh out the commit message with as much information as possible to help future you (or team members) get a clear picture of the work done by looking at your commit alone.&lt;/p&gt;

&lt;p&gt;The commit subject, as seen below, has the convention of starting with the type of work being committed (bug, feature, housekeeping, etc) all in lowercase, followed by a semicolon, space and commit message, starting capitalised, in the imperative form, without any full stop at the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type: [Ticket#](Optional) Summary of change

# **--- Proposed Changes ---**
#
#
# Issue / Task: [name](url)

# --- COMMIT END --- (informational only, do not uncomment)
# &amp;lt;type&amp;gt;: &amp;lt;subject&amp;gt; (Approx 50 characters)
# |&amp;lt;----  Using a Maximum Of 50 Characters  ----&amp;gt;|
# Explain why this change is being made
# |&amp;lt;----   Try To Limit Each Line to a Maximum Of 72 Characters   ----&amp;gt;|
# --------------------
# Type can be
#    feature      (new feature)
#    fix          (bug fix)
#    style        (UI / UX style changes)
#    test         (adding or refactoring tests; no production code change)
#    housekeeping (refactoring code or adding documentation)
#    devops       (background / architecture changes)
# --------------------
# Remember to
#   - Use the imperative mood in the subject line
#   - Do not end the subject line with a period
#   - Separate subject from body with a blank line
#   - Use the body to explain what and why vs. how
# --------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the lines starting with a &lt;code&gt;#&lt;/code&gt; are commented out and will not be displayed when viewing the commit message so if you intend to add future detail to your commit message, be sure to uncomment the lines necessary.&lt;/p&gt;

&lt;p&gt;You can set up your default git commit message template with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git config --global commit.template ~/.gitmessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example commit messages:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature: Add user authentication
fix: About page infinite redirect

// or

feature: [21] Adds user authentication
fix: [42] About page infinite redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Very often when jumping into a piece of work, I might not know right off the bat what the commit message will be as it tends to evolve as the work gets completed. In this case, I usually start my commit with something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wip: Not sure what the issue is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and later on, once having completed the work and getting the branch ready for review, rename the commit messages appropriately during an interactive rebase (&lt;code&gt;git rebase -i&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Comments
&lt;/h2&gt;

&lt;p&gt;This is by no means the definitive git convention standard to employ and it is something that is continually evolving and morphing as I find myself working on new and different projects. Everyone's experience is different and must be dealt with accordingly but, perhaps there are a few nuggets here that could help you guide a more structured and concise git workflow.&lt;/p&gt;

&lt;p&gt;To further the conventions, I have also put together standard workflows, processes, and templates when it comes to GitHub Pull Requests (PR) and Issues affecting code reviews, QA testing, and deployment, but that is a story for another post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;1.) A lot of the inspiration for these conventions has been taken from "Conventional Commits".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.conventionalcommits.org/en/v1.0.0/#why-use-conventional-commits" rel="noopener noreferrer"&gt;https://www.conventionalcommits.org/en/v1.0.0/#why-use-conventional-commits&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcodenisi.dev/en/blog/why-you-should-use-conventional-commits/" rel="noopener noreferrer"&gt;https://marcodenisi.dev/en/blog/why-you-should-use-conventional-commits/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2.) A classic article on how best to write commit messages. I have gleaned a lot of the commit message advice in this article to supplement my variation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cbea.ms/git-commit/#imperative" rel="noopener noreferrer"&gt;https://cbea.ms/git-commit/#imperative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.) A commit linting tool you can incorporate into your workflow to enforce a standard of commit messages. Can be configured in a pre-commit GIT hook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://commitlint.js.org/#/" rel="noopener noreferrer"&gt;https://commitlint.js.org/#/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Feature Flags | Controlled Chaos in Production</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Tue, 09 Sep 2025 14:04:25 +0000</pubDate>
      <link>https://dev.to/brazenbraden/feature-flags-controlled-chaos-in-production-3hie</link>
      <guid>https://dev.to/brazenbraden/feature-flags-controlled-chaos-in-production-3hie</guid>
      <description>&lt;p&gt;Feature flags are a way of life when developing software. They let you deploy code to production without immediately exposing it to users — like flipping a switch without blowing the fuse.&lt;/p&gt;

&lt;p&gt;In practice, they're used to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ship features incrementally&lt;/li&gt;
&lt;li&gt;Test changes in production with internal users&lt;/li&gt;
&lt;li&gt;Run A/B tests or multivariate experiments&lt;/li&gt;
&lt;li&gt;Deliver bespoke functionality to specific customers (not my favourite approach, but hey, sometimes we have to pick our battles)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feature flags introduce flexibility — but also the responsibility to manage them well. Left unchecked, they can become a tangle of forgotten toggles, quietly rotting your codebase from within.&lt;/p&gt;

&lt;p&gt;Let’s take a look at one of the more robust feature flag services out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  LaunchDarkly
&lt;/h2&gt;

&lt;p&gt;One of the more popular services (at least in my limited experience) is &lt;a href="https://launchdarkly.com/" rel="noopener noreferrer"&gt;LaunchDarkly&lt;/a&gt;. It allows you to create and manage feature flags, define targeting rules (e.g., which users get what), and view flag usage analytics — including which flags are no longer being used.&lt;/p&gt;

&lt;p&gt;That last bit is more useful than it sounds. Stale flags can linger for months or even years, adding dead weight to your app and making the logic harder to follow. Being able to identify and prune them is a big plus.&lt;/p&gt;

&lt;p&gt;Here’s a basic integration pattern for using LaunchDarkly in a Rails app:&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;FeatureFlag&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;
      &lt;span class="nb"&gt;self&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;feature_a?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"feature-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# this default will make sense shortly&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feature_b?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"feature-b"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feature_c?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# hard-coded flag (sometimes needed)&lt;/span&gt;

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

    &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:entity&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;

      &lt;span class="n"&gt;ld_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ld_client&lt;/span&gt;
      &lt;span class="vi"&gt;@ld_client&lt;/span&gt; &lt;span class="o"&gt;||=&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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ld_client&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;entity_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LDContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# a unique identifier for the entity&lt;/span&gt;
          &lt;span class="ss"&gt;kind: &lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;       &lt;span class="c1"&gt;# usually "user", but could be "account", etc.&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern gives you a central place to check feature availability in your app. You might wire this into your controllers, services, or views like so:&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="no"&gt;FeatureFlag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;feature_a?&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new_feature_version&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:legacy_version&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 readable, testable, and keeps your flag logic out of the weeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Flags in Test and Development Environments
&lt;/h2&gt;

&lt;p&gt;Since LaunchDarkly is an external service that communicates via HTTP, you definitely don’t want your test suite making live requests every time it runs. That’s just asking for flakiness and slow feedback loops.&lt;/p&gt;

&lt;p&gt;Instead, we can use a mock client in the test environment to simulate feature flag behavior without hitting the actual API. Here’s a simple example I popped into our &lt;code&gt;lib&lt;/code&gt; folder:&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;LaunchDarkly&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MockLdClient&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialized?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_key&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;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default&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;This mock client always returns the &lt;code&gt;default&lt;/code&gt; value passed into the &lt;code&gt;variation&lt;/code&gt; method — making it trivial to control feature behavior explicitly in your tests.&lt;/p&gt;

&lt;p&gt;But wait — why not just put LaunchDarkly into &lt;code&gt;"offline"&lt;/code&gt; mode in the test environment, like we do below for development?&lt;/p&gt;

&lt;p&gt;Good question. While &lt;code&gt;offline: true&lt;/code&gt; does prevent real network calls, it still requires the full LaunchDarkly SDK to be initialized — which adds unnecessary overhead and dependencies to your test suite. By using a lightweight mock client instead, you get a few key benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster tests&lt;/strong&gt; — no SDK initialization, no config loading, just a plain Ruby object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero external dependencies&lt;/strong&gt; — the mock removes the need to load the LaunchDarkly gem at all in tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler control&lt;/strong&gt; — it's easier to stub or extend the mock for edge cases if needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt; — your tests won't break due to SDK upgrades or changes in default behaviors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the mock client keeps your test environment lean and focused. It’s a small abstraction that pays for itself in speed and stability.&lt;/p&gt;

&lt;p&gt;With that in place, in your LaunchDarkly initializer (&lt;code&gt;config/initializers/launch_darkly.rb&lt;/code&gt;), you can wire it in like so:&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="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&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;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/lib/launch_darkly/mock_ld_client.rb"&lt;/span&gt;

&lt;span class="k"&gt;case&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;env&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"test"&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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ld_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MockLdClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FAKE API KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;offline: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ld_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LDClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'LD_KEY'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ld_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LDClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'LD_KEY'&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;In the &lt;strong&gt;test&lt;/strong&gt; environment, we avoid any HTTP calls by using the mock client.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;development&lt;/strong&gt;, we set LaunchDarkly into &lt;code&gt;offline&lt;/code&gt; mode. This disables all outbound network traffic and ensures your app doesn't hang or fail due to a misconfigured API key. You can still control the outcome of flags by setting the default values in your FeatureFlag methods — which is usually more than enough for local dev.&lt;/p&gt;

&lt;p&gt;This setup gives you full control and isolation in non-prod environments, with zero reliance on external calls. Win-win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Other Options
&lt;/h2&gt;

&lt;p&gt;While this post focuses on LaunchDarkly, there are plenty of other options — some open-source, some more opinionated, and some simpler to get going with.&lt;/p&gt;

&lt;p&gt;Here are a few worth considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/flippercloud/flipper" rel="noopener noreferrer"&gt;Flipper&lt;/a&gt; – Powerful, flexible, and actively maintained. Can use a variety of backends (Redis, ActiveRecord, etc.) and integrates with a slick UI via Flipper Cloud.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/FetLife/rollout" rel="noopener noreferrer"&gt;Rollout&lt;/a&gt; – A battle-tested gem that’s been around for a while. Simpler API, great for percentage rollouts and user targeting.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/voormedia/flipflop" rel="noopener noreferrer"&gt;Flipflop&lt;/a&gt; – Great for toggling features via a web UI. Good for admin-driven flags or feature previews.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one to use depends on your needs — whether you want self-hosted vs. SaaS, need advanced targeting and analytics, or just want a simple way to toggle features in development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parting Thoughts
&lt;/h2&gt;

&lt;p&gt;Feature flags give you control, flexibility, and faster feedback loops — but like anything powerful, they need to be managed responsibly. Treat them like temporary scaffolding, not permanent fixtures. Set reminders to clean them up. Document what each flag does and who owns it. And when possible, limit the number of active flags in your system at a given time.&lt;/p&gt;

&lt;p&gt;If you’ve had war stories with forgotten flags or misconfigured rollouts, I’d love to hear them. Otherwise, happy toggling.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Better Sidekiq Classes</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Thu, 28 Aug 2025 22:12:04 +0000</pubDate>
      <link>https://dev.to/brazenbraden/better-sidekiq-classes-430b</link>
      <guid>https://dev.to/brazenbraden/better-sidekiq-classes-430b</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/sidekiq/sidekiq" rel="noopener noreferrer"&gt;Sidekiq&lt;/a&gt; is pretty much the go-to solution for enqueuing jobs for background processing when working on a Ruby-based project. It's simple to implement, has a clear DSL, and is well-supported by common testing frameworks like RSpec. That being said, and I may be nitpicking, but the standard implementation examples do add a little extra overhead if you wish to be more flexible with your job business logic, and additional test setup is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "normal" way
&lt;/h2&gt;

&lt;p&gt;Given we want a little background job that updates a user's name and any other business logic needed (to keep the example simple), we could create a standard Sidekiq job with that logic and call it from our code as such:&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;UpdateUserJob&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Job&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&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="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# .. additional business logic here&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# perform the job as soon as possible&lt;/span&gt;
&lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# delay the processing by 5 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple enough. But what if we want to test the logic being performed inside this job? Firstly, we'd need to update our &lt;code&gt;spec_helper&lt;/code&gt; to let it know what to do with a job. If you're using RSpec, you might even need to add the &lt;a href="https://github.com/philostler/rspec-sidekiq" rel="noopener noreferrer"&gt;rspec-sidekiq&lt;/a&gt; gem for additional configuration and matcher options. A classic-looking spec for this job might look something like 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="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"updates the user"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bob"&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;We need to &lt;code&gt;drain&lt;/code&gt; the job before we can test the result of its work. Alternatively, we could wrap the test inside one of the Sidekiq testing helper methods:&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;it&lt;/span&gt; &lt;span class="s2"&gt;"updates the user"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inline!&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does work as expected however it does leave a little to be desired. We have to remember to do this whenever testing job logic (the number of times I've had to search through previous specs to remind myself of the syntax), it adds a few extra lines to the tests which are not necessary and there is a spec performance hit as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "use case" way
&lt;/h2&gt;

&lt;p&gt;So, what options do we have? Well, the first option would be to extract the logic performed by the job into a class which we would then pass the job attributes to for processing. That way we can independently test the business logic without having to mess with background workers and the like. I have used various names for these classes, &lt;strong&gt;Use Case&lt;/strong&gt;, &lt;strong&gt;Resource&lt;/strong&gt;, or &lt;strong&gt;Library&lt;/strong&gt; to name a few (use whatever best suits you and your conventions). Our new job would look something like 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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserJob&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Job&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;UpdateUserUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserUseCase&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user_id&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="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="vi"&gt;@name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# .. additional business logic here&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, when it comes to testing, we could just forgo testing the actual job class as it does nothing except pass on some arguments to a plain old Ruby object. We can just test the class directly. Sidekiq is also already extensively tested so there is little point in testing whether the jobs on the queue have increased or not unless you are expecting new jobs to be created as a knock-on effect of your business logic.&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;it&lt;/span&gt; &lt;span class="s2"&gt;"updates the user"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;UpdateUserUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bob"&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;The added benefit to doing it this way is that you have a plain old Ruby object that you can call independently and inline in your code elsewhere should the need arise. For instance, you may want to defer the processing to the background when handling a mass import of data, however, when being performed as a single HTTP request to your API, you could wish to do it inline.&lt;/p&gt;

&lt;p&gt;The downside to this is that we now, however, have had to create an additional class to provide this flexibility. That means, if we wish to keep this convention going, for each &lt;code&gt;Job&lt;/code&gt; class, there would be a corresponding &lt;code&gt;UseCase&lt;/code&gt; class. What if we could take this idea one step further? What if we could turn our Job class into a plain old Ruby object right from the start? Let's rewrite our &lt;code&gt;UpdateUserJob&lt;/code&gt; class.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "runner" way (with POROs)
&lt;/h2&gt;

&lt;p&gt;Let's pretend you had never heard of Sidekiq or background queues, and you want to write some code to perform a specific task. For the sake of convention, I have ended the following class name with "Job" to match previous examples however by syntax definition, it is no longer what we would call a classic "job". It is now just a Plain Old Ruby Object (PORO; identical to our previous &lt;code&gt;UpdateUserUseCase&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user_id&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="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="vi"&gt;@name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# .. additional business logic here&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;With that in place, we need to set up some sort of mechanism to have this code be run by Sidekiq in our background queue. We can introduce a simple class to wrap the sidekiq calls, allowing us to inject our custom job class with its arguments. To not conflict with the Sidekiq namespace, I have created the &lt;code&gt;Runners&lt;/code&gt; module which will run this code for me &lt;em&gt;(side note: no matter what project I've worked on in the past, I always seem to use these "runner" style classes for various things)&lt;/em&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;module&lt;/span&gt; &lt;span class="nn"&gt;Runners&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sidekiq&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Job&lt;/span&gt;

    &lt;span class="n"&gt;sidekiq_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;retry: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;backtrace: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;queue: &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&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;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_in_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&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;perform_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_in_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we have everything we need to be able to throw plain old Ruby classes into the background for processing. If we wish to run our business logic inline, we simply run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but if we want to delegate the processing to the background queue, we can send it off immediately or delay 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="no"&gt;Runners&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# perform the job as soon as possible&lt;/span&gt;
&lt;span class="no"&gt;Runners&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UpdateUserJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# delay the processing by 5 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0qg0e63pe5tvzedbljf3.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%2F0qg0e63pe5tvzedbljf3.png" alt="Sidekiq Web View" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bells &amp;amp; Whistles
&lt;/h2&gt;

&lt;p&gt;Now that we have our lovely Sidekiq runner class, we can embellish it with a couple final bells and whistles. We might want to add / override the sidekiq_options on demand instead of being stuck with the previous hard-coded options. Perhaps we prefer using keyword arguments in our method definitions for the added clarity and ability to insert the arguments in any order. And maybe, we would like the job names in the Sidekiq UI to be a little more descriptive as to the actual job they are running so that we don't have to go digging through the arguments list to find specific jobs.&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;Runners&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sidekiq&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Job&lt;/span&gt;

    &lt;span class="no"&gt;DEFAULT_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;retry: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;backtrace: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;queue: &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# This handles the case of keyword arguments&lt;/span&gt;
        &lt;span class="n"&gt;job_class_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constantize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symbolize_keys!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="c1"&gt;# This handles the case of basic arguments&lt;/span&gt;
        &lt;span class="n"&gt;job_class_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constantize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;sidekiq_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DEFAULT_OPTIONS&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;update_display_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&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;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_in_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;update_display_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&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;perform_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_in_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

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

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_display_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_class&lt;/span&gt;&lt;span class="p"&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;sidekiq_options&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="s2"&gt;"display_class"&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;job_class&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="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In essence, you can now define your background job classes as basic Ruby objects, execute them inline, or transfer them to Sidekiq for background processing. The main requirement is maintaining a consistent DSL with your job classes, such as implementing a mandatory &lt;code&gt;call&lt;/code&gt; (or equivalent: &lt;code&gt;perform&lt;/code&gt; or &lt;code&gt;execute&lt;/code&gt;) method. (Kudos to my friend &lt;a href="https://github.com/krzyczak" rel="noopener noreferrer"&gt;Kris&lt;/a&gt; for the original idea.)&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JuggleBee’s Great Leap – Data Migration, ActiveStorage, and Production Readiness (Part 2)</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Mon, 18 Aug 2025 06:41:24 +0000</pubDate>
      <link>https://dev.to/brazenbraden/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2-5110</link>
      <guid>https://dev.to/brazenbraden/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2-5110</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In case you missed &lt;strong&gt;Part 1&lt;/strong&gt;, we covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why I started fresh on &lt;strong&gt;Rails 8&lt;/strong&gt; instead of doing incremental upgrades.&lt;/li&gt;
&lt;li&gt;Migrating core &lt;strong&gt;models/controllers/services&lt;/strong&gt; to modern Rails conventions.&lt;/li&gt;
&lt;li&gt;Swapping Sprockets for &lt;strong&gt;importmaps&lt;/strong&gt; and modernizing the JavaScript setup.&lt;/li&gt;
&lt;li&gt;Replacing &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; with &lt;strong&gt;ActiveJob + SolidQueue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Moving deployments to &lt;strong&gt;Kamal 2&lt;/strong&gt; with Traefik and Let’s Encrypt.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Catch up on it here:&lt;/strong&gt; &lt;a href="https://dev.to/brazenbraden/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1-1goj"&gt;JuggleBee’s Great Leap – Rebuilding a Rails 4 App in Rails 8 (Part 1)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now—back to the post.&lt;/p&gt;




&lt;p&gt;Now that we had a deployable app, I set up a staging server using my new favourite provider, &lt;a href="https://www.hetzner.com/" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; (&lt;em&gt;not a sponsor, lol&lt;/em&gt;), spun it up, and witnessed a “fully functional” skeleton of JuggleBee. Stage 1 of the migration plan was complete.&lt;/p&gt;

&lt;p&gt;Now came the real meat and potatoes of the project — actually hydrating the new app with all of its production data. We’re talking over 50,000 images, countless database records, and security credentials… the works. This is the part where a missed step could mean broken listings, missing files, or downtime. No pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Daunting Database Migration
&lt;/h2&gt;

&lt;p&gt;Legacy JuggleBee had been coasting along happily on Postgres 9.6 — which, back in 2016, was the latest and greatest. Fast forward to today and it’s long past its sell-by date, with official support ending in November 2021. Upgrading wasn’t just about keeping up with the times; Postgres 17.5 brings some serious perks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native JSON field type support.&lt;/li&gt;
&lt;li&gt;Incremental backups with &lt;code&gt;pg_basebackup --incremental&lt;/code&gt;, slashing backup time and storage usage.&lt;/li&gt;
&lt;li&gt;Noticeable performance gains from better memory management, indexing, and partitioning.&lt;/li&gt;
&lt;li&gt;Improved observability via enhanced &lt;code&gt;EXPLAIN&lt;/code&gt; options and richer stats views.&lt;/li&gt;
&lt;li&gt;…and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was a no-brainer to upgrade, but the version gap meant one wrong move could corrupt thousands of user, auction, and invoice records. That made reliable backups non-negotiable before touching a single byte. Luckily, I already had a manual backup process baked into my deploy routine. Here’s the script (&lt;em&gt;feel free to steal any of the scripts in my posts&lt;/em&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="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;DB_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps | &lt;span class="nb"&gt;grep &lt;/span&gt;postgres | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $NF}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dump_&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%d-%m-%Y&lt;span class="s2"&gt;"_"&lt;/span&gt;%H_%M_%S&lt;span class="sb"&gt;`&lt;/span&gt;.sql
&lt;span class="nv"&gt;BACKUP_FOLDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db_backups
&lt;span class="nv"&gt;KEEP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10

&lt;span class="c"&gt;# Dump the database into a backup sql file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating the database dump..."&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$DB_CONTAINER&lt;/span&gt; pg_dumpall &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_NAME&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Database dump created."&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Moving database backup to backup folder..."&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FOLDER&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_NAME&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FOLDER&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup moved to backup folder."&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deleting old releases..."&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FOLDER&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$KEEP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | xargs &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup stored."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One catch: &lt;code&gt;pg_dumpall&lt;/code&gt; (used above) tries to dump &lt;em&gt;everything&lt;/em&gt; — including Postgres cluster-wide settings — which can be version-specific and cause headaches on import. For a major version jump, you want &lt;code&gt;pg_dump&lt;/code&gt;, which targets a single database and avoids the version mismatch landmines.&lt;/p&gt;

&lt;p&gt;For the migration, I only needed to change the &lt;code&gt;docker&lt;/code&gt; command we executed - with the added ENV vars, that is:&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;DB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db_user
&lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db_password
&lt;span class="nv"&gt;DATABASE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;database_name

&lt;span class="c"&gt;# ...&lt;/span&gt;

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DB_PASSWORD&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$DB_CONTAINER&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nv"&gt;$DB_USER&lt;/span&gt; &lt;span class="nv"&gt;$DATABASE_NAME&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the new backup in hand, importing into Postgres 17.5 went off without a hitch — no edits, no drama. Honestly, I was braced for a fight given the version gap, but the data was simple enough (and avoided fancy Postgres features) that it just… worked. A rare and pleasant surprise in a project full of moving parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing Image Migration
&lt;/h2&gt;

&lt;p&gt;Unlike the data migration before it, migrating all the listing images (just shy of 50,000!) came with its own set of headaches. The legacy setup used CarrierWave with a custom uploader and fixed S3 paths, which worked fine at the time but wasn’t exactly future-proof. Since I was already upgrading to Rails 8, it made sense to tackle this technical debt head-on and move to ActiveStorage — effectively retiring these gems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;carrierwave&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fog-aws&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mini_magick&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;… and replacing them with &lt;code&gt;image_processing&lt;/code&gt; for post-processing.&lt;/p&gt;

&lt;p&gt;In the old days, uploads required creating an “Uploader” class and mounting it on a model. 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="c1"&gt;# app/models/image.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;mount_uploader&lt;/span&gt; &lt;span class="ss"&gt;:aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ImageUploader&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/uploaders/image_uploader.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageUploader&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;CarrierWave&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Uploader&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CarrierWave&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MiniMagick&lt;/span&gt;
  &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="ss"&gt;:fog&lt;/span&gt;

  &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="ss"&gt;resize_to_fill: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="ss"&gt;:thumb&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="ss"&gt;resize_to_fill: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store_dir&lt;/span&gt;
    &lt;span class="s2"&gt;"listings/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listing_id&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;model&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="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;With ActiveStorage, uploaders and hard-coded S3 paths are gone. You simply declare the attachment in your model (after setting up &lt;code&gt;config/storage.yml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_one_attached&lt;/span&gt; &lt;span class="ss"&gt;:file&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;attachable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;attachable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt; &lt;span class="ss"&gt;:thumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;resize_to_limit: &lt;/span&gt;&lt;span class="no"&gt;THUMB_DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preprocessed: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That takes care of &lt;em&gt;future&lt;/em&gt; uploads — but what about migrating the 50,000+ legacy images stored in the old bucket? The &lt;code&gt;image.aws&lt;/code&gt; field plus CarrierWave’s &lt;code&gt;store_dir&lt;/code&gt; gave me the exact file path to each image. I then wrote a script to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download each image from the legacy bucket.&lt;/li&gt;
&lt;li&gt;Re-upload it through ActiveStorage so it generates its own hashed storage path.&lt;/li&gt;
&lt;li&gt;Skip already-migrated images to allow safe re-runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the script in all its glory (run synchronously to avoid AWS rate limits — which meant a cool 4 hours of migration time):&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;#!/usr/bin/env ruby&lt;/span&gt;

&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../config/environment'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'aws-sdk-s3'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'stringio'&lt;/span&gt;

&lt;span class="no"&gt;OLD_S3_BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:old_aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;OLD_S3_REGION&lt;/span&gt; &lt;span class="o"&gt;=&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:old_aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;OLD_S3_ACCESS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:old_aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:s3_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;OLD_S3_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:old_aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:s3_secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: skip already migrated images&lt;/span&gt;
&lt;span class="no"&gt;SKIP_EXISTING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Aws&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;S3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;region: &lt;/span&gt;&lt;span class="no"&gt;OLD_S3_REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;access_key_id: &lt;/span&gt;&lt;span class="no"&gt;OLD_S3_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;secret_access_key: &lt;/span&gt;&lt;span class="no"&gt;OLD_S3_SECRET_ACCESS_KEY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Starting image migration..."&lt;/span&gt;

&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_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;image&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;SKIP_EXISTING&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attached?&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Skipping Image &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&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; (already attached)"&lt;/span&gt;
      &lt;span class="k"&gt;next&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Construct old S3 key from uploader logic - your path here&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"listings/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listing_id&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;image&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="si"&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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:aws&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;bucket: &lt;/span&gt;&lt;span class="no"&gt;OLD_S3_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;io: &lt;/span&gt;&lt;span class="no"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&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="ss"&gt;filename: &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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_type&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'image/jpeg'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Migrated Image &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&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; (Listing &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listing_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Aws&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;S3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NoSuchKey&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="nb"&gt;warn&lt;/span&gt; &lt;span class="s2"&gt;"Image &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&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; missing in old bucket: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="nb"&gt;warn&lt;/span&gt; &lt;span class="s2"&gt;"Failed to migrate Image &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image&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="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&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;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Migration script finished"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four hours later, we were in business — all images were now managed by ActiveStorage.&lt;br&gt;
Future uploads are handled automatically, thumbnails are generated on demand, and the whole setup is cleaner and easier to maintain.&lt;/p&gt;
&lt;h2&gt;
  
  
  Credentials &amp;amp; Security
&lt;/h2&gt;

&lt;p&gt;You might have noticed the use of &lt;code&gt;Rails.application.credentials&lt;/code&gt; in the previous script. That’s new territory — Rails encrypted credentials didn’t exist back in 4.2.&lt;/p&gt;

&lt;p&gt;Back then we had &lt;code&gt;secrets.yml&lt;/code&gt;, which stored everything in plain text and was meant to be excluded from Git. In theory, you’d inject it during deploy with whatever tooling you used. In practice… I was lazy. My deploys were manual, the repo was private, and I just checked them straight in. &lt;strong&gt;Big no-no.&lt;/strong&gt; Thankfully, Bitbucket never got hacked — but let’s just say this setup wouldn’t pass any kind of security audit.&lt;/p&gt;

&lt;p&gt;Rails 5.2 introduced encrypted credentials — finally, a way to store secrets in plain text &lt;em&gt;locally&lt;/em&gt; while Rails encrypts them with a master key. That master key stays out of Git and gets injected during deploy. Much better.&lt;/p&gt;

&lt;p&gt;Fast forward to today, and Kamal (my de facto deployment tool) takes it a step further with &lt;a href="https://kamal-deploy.org/docs/commands/secrets/" rel="noopener noreferrer"&gt;built-in support for pulling secrets&lt;/a&gt; straight from password managers like Bitwarden, 1Password, or LastPass. That means my Rails master key, database credentials, and other environment variables can live safely in Bitwarden, and Kamal just grabs them during deploy. No manual copying, no “where did I put that key?” moments, and no plain-text secrets hanging around in the repo.&lt;/p&gt;

&lt;p&gt;Here’s an example using Bitwarden via the CLI. This snippet fetches all secrets and assigns them to variables in &lt;code&gt;.kamal/secrets&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="nv"&gt;SECRETS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kamal secrets fetch &lt;span class="nt"&gt;--adapter&lt;/span&gt; bitwarden-sm all&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kamal secrets extract KAMAL_REGISTRY_PASSWORD &lt;span class="nv"&gt;$SECRETS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kamal secrets extract RAILS_MASTER_KEY &lt;span class="nv"&gt;$SECRETS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kamal secrets extract POSTGRES_USER &lt;span class="nv"&gt;$SECRETS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kamal secrets extract POSTGRES_PASSWORD &lt;span class="nv"&gt;$SECRETS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only catch: you’ll need to have the Bitwarden CLI installed and authenticated on every machine you deploy from. It’s a one-time setup, but totally worth it for the peace of mind of never having to worry about leaked secrets again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Looking back, a few big takeaways stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scripts are your best friend.&lt;/strong&gt; Whether it’s a database dump, image migration, or credentials setup, having a repeatable script means you can run it, tweak it, and run it again without reinventing the wheel. It also removes a ton of mental overhead when you’re juggling multiple moving parts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test migrations in staging — thoroughly.&lt;/strong&gt; Moving to PostgreSQL 17.5 could have been a disaster if I’d gone straight into production. Running the full migration end-to-end in a safe environment gave me confidence and caught subtle issues before they ever touched live data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expect the cleanup phase.&lt;/strong&gt; Even after the “big ticket” migrations were done, staging revealed a laundry list of smaller fixes — broken image links, JavaScript load order issues, outdated gem calls, and other gremlins. None of these were major individually, but together they took nearly two weeks to iron out. Build that buffer into your timeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take the opportunity to ditch technical debt.&lt;/strong&gt; CarrierWave, fog-aws, Sidekiq, Redis… all of these had served me well for years, but replacing them with ActiveStorage and SolidQueue means fewer dependencies to maintain, fewer services to monitor, and a cleaner architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security deserves equal billing with features.&lt;/strong&gt; Rails encrypted credentials plus Bitwarden CLI integration finally dragged my secrets management into the modern age. No more plain-text secrets in Git. Enough said.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modern Rails is worth the leap.&lt;/strong&gt; From importmaps to Kamal, the tooling around Rails 8 makes deployment, scaling, and maintenance dramatically simpler than what I was doing in 2015.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;And with that, JuggleBee officially joined the modern Rails era.&lt;/p&gt;

&lt;p&gt;This wasn’t just a version bump — it was a full-stack rejuvenation.&lt;br&gt;
The app runs faster, the infrastructure is leaner, and I finally have a deployment pipeline I can trust at 2 a.m. without crossing my fingers.&lt;/p&gt;

&lt;p&gt;Was it worth skipping incremental upgrades? Absolutely. Starting fresh on Rails 8 gave me a clean slate to rebuild only what mattered, using tools that will keep JuggleBee humming for years to come.&lt;/p&gt;

&lt;p&gt;If you’ve been sitting on a legacy Rails app, staring down the upgrade path and wondering if it’s worth the effort — it is. Just plan your migrations, test them in staging, and embrace the opportunity to shed some old weight while you’re at it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.jugglebee.com" rel="noopener noreferrer"&gt;JuggleBee’s&lt;/a&gt; now live on &lt;strong&gt;Rails 8 / Ruby 3.4.3 / PostgreSQL 17.5&lt;/strong&gt;, with ActiveStorage handling thousands of images, Bitwarden keeping our secrets safe, and Kamal making deployments a one-command affair.&lt;/p&gt;

&lt;p&gt;Now it’s time to step away from the terminal, grab a cold beer, and actually enjoy the thing I’ve been rebuilding over the last month — until the next big upgrade comes along.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>refactoring</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JuggleBee’s Great Leap - Rebuilding a Rails 4 App in Rails 8 (Part 1)</title>
      <dc:creator>Braden King</dc:creator>
      <pubDate>Wed, 13 Aug 2025 14:49:18 +0000</pubDate>
      <link>https://dev.to/brazenbraden/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1-1goj</link>
      <guid>https://dev.to/brazenbraden/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1-1goj</guid>
      <description>&lt;p&gt;&lt;a href="http://www.jugglebee.com" rel="noopener noreferrer"&gt;JuggleBee&lt;/a&gt; was born in 2015. It was Namibia's first online auction platform and is still one of the biggest today. I built it with Ruby on Rails 4.2 on Ruby 2.2. It sat there, rock-solid and stubbornly stable, only needing the occasional dust-off and minor tweak over the years. At various points in time, I thought about upgrading the various versions; however, I was stuck between a rock and a hard place — in order to upgrade Ruby, I had to upgrade Rails, and vice versa. The idea of incremental upgrades felt like a nightmare — two battlefronts to fight on, Ruby on one side and Rails on the other.&lt;/p&gt;

&lt;p&gt;Fast forward to 2025. With the arrival of my daughter, I acquired a smattering of free time interspersed with sleepless nights. The perfect time to finally bite the bullet, bring JuggleBee into the 21st century (you know what I mean), and get these upgrades done. After much pondering and planning, only one realistic option remained... incremental upgrades would take months with constant patching between every version bump; I would have to take the leap from Rails 4.2 straight to Rails 8, with Ruby hopping from 2.2 to 3.4.3. It was quite the journey, and what follows is a breakdown of the challenges and learnings I made along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Fresh
&lt;/h2&gt;

&lt;p&gt;Not to blow my own horn or anything, but I was in a pretty good place with this migration plan. The codebase of JuggleBee was intentionally built with self-contained business logic components, defining a clear separation of concerns. The models and controllers were not overly bloated, with liberal usage of "service objects" performing the more complicated logic, and an added separation of logic between my controllers and views using the "view decorator" pattern. This architecture made the copy-pasta approach almost… dare I say… fun. But let’s not kid ourselves — a jump from Rails 4.2 to 8 is no walk in the park.&lt;/p&gt;

&lt;p&gt;As part of the &lt;code&gt;rails new&lt;/code&gt; process (after installing the latest Ruby and necessary Rails gems), and before the true code migration could begin, I just needed an empty, runnable project with the necessary gems installed. I appended my old &lt;code&gt;Gemfile&lt;/code&gt; with the auto-generated one, removing all locked versions to ensure the latest compatible versions were installed. I also identified irrelevant and dead gems, removing those and replacing them with alternatives where necessary (more detail on this later).&lt;/p&gt;

&lt;p&gt;Reading into each of the default gems specified by the &lt;code&gt;rails new&lt;/code&gt; generator gave me a good understanding of what the current Rails stack looks like. I didn’t need all of it, so some were commented out, but I did take some of the new suggestions into account. &lt;code&gt;Puma&lt;/code&gt;, for example, replaced &lt;code&gt;Unicorn&lt;/code&gt;, which I was previously using. I could also get rid of &lt;code&gt;God&lt;/code&gt;, in the non-divine sense, as I no longer needed to monitor my server state manually. All in all, this whole process didn’t take more than an hour or so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Models, Controllers, Services &amp;amp; Views (in that order)
&lt;/h3&gt;

&lt;p&gt;These being purely Ruby code made it the easiest place to start. Even though we jumped up from Ruby 2.2 to 3.4.3, not all that much in the core language changed. Twas Rails that was the most behind. For example, JuggleBee was developed before the abstract &lt;code&gt;ApplicationRecord&lt;/code&gt; class made its way into the Rails architecture!&lt;/p&gt;

&lt;h4&gt;
  
  
  Models
&lt;/h4&gt;

&lt;p&gt;The models were the easiest to migrate over, as they already had a smattering of unit tests behind them. First, though, we had to get the database up and running. Spoiler alert: I was also updating our Postgres version from &lt;strong&gt;9.6&lt;/strong&gt; to &lt;strong&gt;17.5&lt;/strong&gt; as part of this big update. The old migration files were fossils — brittle, outdated, and utterly unfit for a modern Rails 8 app. The simplest solution was to drop a &lt;code&gt;pg_dump&lt;/code&gt; of the current database (just on my local copy of the now legacy JuggleBee) and import it, generating the &lt;code&gt;schema.rb&lt;/code&gt;. I could now copy across my models and test them out in the &lt;code&gt;rails console&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some of the updates required to get things up and running were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;belongs_to&lt;/code&gt; is now a required atribute. I have a home-grown version of polymorphism which allows a &lt;code&gt;Listing&lt;/code&gt; to be either a &lt;code&gt;Product&lt;/code&gt; or &lt;code&gt;Auction&lt;/code&gt; and the &lt;code&gt;belongs_to&lt;/code&gt; on these associations are optional, so I had to update these associations accordingly (&lt;em&gt;**side note&lt;/em&gt;&lt;em&gt;: a refactor waiting to happen&lt;/em&gt;). So,
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:auction&lt;/span&gt;
&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:product&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes&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;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:auction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;optional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;optional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I had a couple of instances of &lt;code&gt;update_attributes&lt;/code&gt; (very selective. Given they bypass validations, they were used with extreme caution) which is now deprecated and had to be changed to &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deprecated validation methods such as &lt;code&gt;validates_presence_of :name&lt;/code&gt; needed updating to &lt;code&gt;validates :name, presence: true&lt;/code&gt; sort of changes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for the views, the gems I used (&lt;code&gt;haml-rails&lt;/code&gt;, &lt;code&gt;redcarpet&lt;/code&gt;, &lt;code&gt;simple_form&lt;/code&gt;, &lt;code&gt;cocoon&lt;/code&gt;, etc) are all still actively maintained and required no changes at all — a very pleasant surprise.&lt;/p&gt;

&lt;h4&gt;
  
  
  Controllers
&lt;/h4&gt;

&lt;p&gt;Next up were the controllers. Thanks to my use of service objects for the bulk of complex logic, migrating controllers was mostly painless. Rails 8 is stricter, and that’s where most of my changes were required. While bringing over controllers, I also began migrating the service objects they called upon, which are POROs and required almost no changes.&lt;/p&gt;

&lt;p&gt;The main updates included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Params passed to background jobs now must be valid JSON. This was especially relevant for controllers triggering email or background tasks.&lt;/li&gt;
&lt;li&gt;Controller &lt;code&gt;filter&lt;/code&gt;s were renamed to &lt;code&gt;action&lt;/code&gt;s (&lt;code&gt;before_filter&lt;/code&gt; → &lt;code&gt;before_action&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Controller hooks like &lt;code&gt;before_action&lt;/code&gt; should come after the method they reference, not at the top of the file (a style guideline enforced in modern Rails apps).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect_to :back&lt;/code&gt; was replaced with &lt;code&gt;redirect_back fallback_location: root_path&lt;/code&gt; (or an appropriate fallback path).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;responders&lt;/code&gt; gem is no longer included in Rails, so &lt;code&gt;respond_with&lt;/code&gt; calls were rewritten using &lt;code&gt;respond_to&lt;/code&gt; blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in all, the MVC migration was more about cleaning things up and embracing modern Rails conventions rather than fighting major incompatibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript Modernization
&lt;/h3&gt;

&lt;p&gt;This is where things started to get spicy — the kind of spicy where you wonder if you just bit off more than you can chew. So much has changed in the front-end assets side of things. JuggleBee leverages &lt;strong&gt;jQuery&lt;/strong&gt; (thankfully I dodged the &lt;em&gt;CoffeeScript&lt;/em&gt; bullet) with Sprockets as the asset pipeline. Modern Rails favours &lt;code&gt;importmaps&lt;/code&gt; as the default asset manager and pipeline, and keeping with the spirit of rejuvenating JuggleBee and bringing it up to speed, I signed up to it too. This allowed me to replace a bunch of gems which added various front-end features, such as &lt;code&gt;chartkick&lt;/code&gt;, &lt;code&gt;masonry&lt;/code&gt; and &lt;code&gt;bootstrap&lt;/code&gt; with the importmaps &lt;code&gt;pin&lt;/code&gt; alternative. Some libraries had to remain as CDN imports in my layout file, due to the order at which these JavaScripts got initialized and loaded by the DOM.&lt;/p&gt;

&lt;p&gt;My suite of JavaScript classes also needed a bit of love and attention. The Sprockets&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;//= require&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;statements were replaced with&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="k"&gt;import&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and my classes now had to &lt;code&gt;export&lt;/code&gt; themselves so that they could be loaded as ES modules. All the files had to be relocated from &lt;code&gt;app/assets/javascripts/&lt;/code&gt; to &lt;code&gt;app/javascript/&lt;/code&gt;. The way we add our JavaScript base file into our application view layout also had to change slightly, going from&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_importmap_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;jQuery&lt;/code&gt; is considered a bit long in the tooth, given the plethora of really solid UI frameworks like Vue and React, I opted to keep my reliance on it for the time being. It still works, is actively maintained, and trying to replace it now would be a whole new kettle of fish. An adventure for another day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background Jobs &amp;amp; Scheduling
&lt;/h2&gt;

&lt;p&gt;This area was one of the biggest wins in the entire migration. JuggleBee had the “classic” background processing setup: &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; (for cron). It worked fine for years, but it always felt a little… heavy, like lugging around a toolbox just to tighten a single screw. Sidekiq required its own container, Redis was a hungry beast that always felt like it was asking for more memory, and &lt;code&gt;whenever&lt;/code&gt; meant fiddling with cron jobs and ensuring everything stayed in sync across deploys. It worked, but it was yet another moving part in an already aging stack.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;SolidQueue&lt;/strong&gt; — a gem that ships with Rails 8, built to replace Sidekiq-like setups with something that is both simpler and more tightly integrated with ActiveJob. The real beauty? &lt;strong&gt;No Redis.&lt;/strong&gt; SolidQueue uses the same database you already have (Postgres in our case) to manage job queues, which meant I could drop an entire service and reclaim a chunk of server memory.&lt;/p&gt;

&lt;p&gt;Here’s what this meant for JuggleBee:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No more Sidekiq configuration, container management, or Redis headaches.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;whenever&lt;/code&gt; gem (used for cron scheduling) could be thrown out too, thanks to SolidQueue’s built-in &lt;code&gt;recurring.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Background jobs now feel like a natural part of Rails — no extra glue required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To put it in perspective, my stack went from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rails App + Sidekiq + Redis + whenever + cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;to&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rails App + SolidQueue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Beautiful, minimal, and a lot less to maintain.&lt;/p&gt;

&lt;p&gt;Another &lt;em&gt;chef’s kiss&lt;/em&gt; moment was how little code I had to change. Most jobs migrated over with minimal tweaks — the biggest adjustment being that job arguments &lt;strong&gt;must be JSON-serializable&lt;/strong&gt;. This meant replacing keyword arguments with hashes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;EmailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MyEmailClass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"from"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;current_user&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="s2"&gt;"subject"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Hello there"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;EmailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MyEmailClass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="n"&gt;current_user&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="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"Hello there"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was one of those rare upgrades where less really is more. Dropping Redis and cron alone made this whole migration feel worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure &amp;amp; Deployment
&lt;/h2&gt;

&lt;p&gt;For years, deployment was powered by a custom shell script I wrote. It wasn’t flashy, but it was reliable and got the job done. It handled atomic releases, cleaned up old deployments, built Docker images, restarted containers, and even maintained a cache for faster updates. For a single-developer project like JuggleBee, it was the perfect balance of simplicity and control.&lt;/p&gt;

&lt;p&gt;Here’s the core of that script:&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;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;master&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/ubuntu
&lt;span class="nv"&gt;repo_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_repo_here
&lt;span class="nv"&gt;revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;master
&lt;span class="nv"&gt;keep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="nv"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;/.git_cache
&lt;span class="nv"&gt;releases&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;/releases
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$releases&lt;/span&gt;
&lt;span class="nv"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$releases&lt;/span&gt;/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nv"&gt;$releases&lt;/span&gt;/ &lt;span class="nt"&gt;-t&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$releases&lt;/span&gt;/&lt;span class="nv"&gt;$timestamp&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;git clone &lt;span class="nv"&gt;$repo_url&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"downloading latest source code from &lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$release&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git gc &lt;span class="nt"&gt;--prune&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git remote prune origin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git fetch origin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; origin/&lt;span class="nv"&gt;$branch&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git archive &lt;span class="nv"&gt;$revision&lt;/span&gt; | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"release timestamp is: &lt;/span&gt;&lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'building docker images...'&lt;/span&gt;
/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"(cd &lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; docker compose -f docker-production.yml build)"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
        if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
                &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'stopping previous containers...'&lt;/span&gt;
                /bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"(cd &lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; docker compose -f docker-production.yml stop &amp;amp;&amp;amp; docker ps -a -q --filter='status=exited' | xargs docker rm)"&lt;/span&gt;
        &lt;span class="k"&gt;fi

        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'restarting application...'&lt;/span&gt;
        /bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"(cd &lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; docker compose -f docker-production.yml up -d)"&lt;/span&gt;

        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deleting old releases...'&lt;/span&gt;
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$releases&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$keep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | xargs &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'creating symlink.'&lt;/span&gt;
        &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nv"&gt;$cwd&lt;/span&gt;/current-release
        &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nv"&gt;$release&lt;/span&gt; &lt;span class="nv"&gt;$cwd&lt;/span&gt;/current-release

        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deploy complete.'&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honestly, this script could have kept soldiering on for years… until &lt;strong&gt;Kamal&lt;/strong&gt; walked in like the cool new kid who makes your old tricks look ancient. It wasn’t broken — far from it — but Kamal is like having a personal DevOps engineer in your terminal. It takes all the things I cared about (Dockerized deploys, atomic releases, rollbacks) and adds extra superpowers I didn’t even realize I needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing Kamal 2
&lt;/h3&gt;

&lt;p&gt;Kamal is a server provisioning and deployment tool built specifically (but not limited to) Rails apps. It automates a ton of tasks that my script didn’t — and one of the most significant changes? &lt;strong&gt;No more Nginx.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kamal ships with a built-in Traefik proxy that handles all your routing and SSL certificates out of the box. No more tinkering with &lt;code&gt;nginx.conf&lt;/code&gt; files or restarting a separate webserver. One less dependency chewing through RAM and one less thing to break at 2 am.&lt;/p&gt;

&lt;p&gt;Here’s what Kamal brings to the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kamal setup&lt;/code&gt;&lt;/strong&gt;: installs Docker, Git, and all required dependencies on your server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kamal deploy&lt;/code&gt;&lt;/strong&gt;: builds your Rails app into a Docker image, pushes it to your registry, and runs it on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traefik proxy with SSL (via Let’s Encrypt)&lt;/strong&gt;: you get HTTPS without lifting a finger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessory management&lt;/strong&gt;: want Postgres or Redis? Kamal spins them up as Docker containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling&lt;/strong&gt;: need more capacity? add another container in deploy.yml and Kamal will route traffic automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The impact of this was huge. I went from managing Docker builds, old release directories, and an Nginx webserver, to just running one command. There’s something almost unsettling about how smooth it is — I felt like I was missing steps at first, like forgetting my keys when leaving the house.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wait, that’s it? Did I actually deploy? Did I forget something? Oh… it’s done already. Huh.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This wasn’t just a convenience win — it was a dependency detox. Between dropping Nginx here and killing Redis with SolidQueue earlier, my infrastructure got a lot lighter, faster, and cheaper to run. Kamal didn’t just evolve my old script — it rendered half of its logic unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Half Way There
&lt;/h2&gt;

&lt;p&gt;With the new Rails 8 foundation in place, JuggleBee has already shed a lot of its old skin. We’ve modernized the models and controllers, swapped out a creaky asset pipeline for importmaps, retired Sidekiq, Redis, and cron in favor of SolidQueue, and traded a homegrown deployment script (solid as it was) for the convenience and power of Kamal 2. The app is lighter, faster to deploy, and much easier to maintain — but we’re not done yet.&lt;/p&gt;

&lt;p&gt;Getting the Rails 8 app running and deployable was only &lt;strong&gt;half the battle&lt;/strong&gt;. Now comes the rest of the journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrating data from a legacy PostgreSQL 9.6 database to a fresh PostgreSQL 17.5 instance.&lt;/li&gt;
&lt;li&gt;Replacing CarrierWave with ActiveStorage and migrating all user-uploaded files into a new S3 bucket.&lt;/li&gt;
&lt;li&gt;Locking down credentials with Rails encrypted secrets and Bitwarden CLI.&lt;/li&gt;
&lt;li&gt;Cleaning up leftover code smells, finalizing test coverage, and polishing the overall performance of the new stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s some of what Part 2 will cover — all the finishing touches, the unexpected hurdles, and the final steps that took JuggleBee from a Rails 4 relic to a lean, modern, and production-ready Rails 8 app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay tuned for Part 2, where the migration story continues.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>refactoring</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
