<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://fiedler.pro/">
  <id>https://fiedler.pro/</id>
  <title>Szymon Fiedler</title>
  <updated>2026-02-05T12:02:35Z</updated>
  <link rel="alternate" href="https://fiedler.pro/" type="text/html"/>
  <link rel="self" href="https://fiedler.pro/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Szymon Fiedler</name>
    <uri>https://fiedler.pro</uri>
  </author>
  <entry>
    <id>tag:fiedler.pro,2026-02-05:/blog/the-timezone-bug-that-hid-in-plain-sight-for-months/</id>
    <title type="html">The timezone bug that hid in plain sight for months</title>
    <published>2026-02-05T12:02:35Z</published>
    <updated>2026-02-05T12:02:35Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/the-timezone-bug-that-hid-in-plain-sight-for-months/" type="text/html"/>
    <content type="html">&lt;h1&gt;The timezone bug that hid in plain sight for months&lt;/h1&gt;

&lt;p&gt;We recently fixed a bug in a financial platform&amp;#39;s data sync that had been silently causing inconsistencies for months. The bug was elegant in its simplicity: checking DST status for &amp;quot;now&amp;quot; when converting historical dates.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;h2&gt;The broken code&lt;/h2&gt;

&lt;p&gt;I found this while debugging a different sync issue — the real bug turned out to be hiding in a helper method I wasn&amp;#39;t even looking at.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date_to_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in_time_zone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TIMEZONE_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone_key&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;formatted_offset&lt;/span&gt;
  &lt;span class="no"&gt;Time&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_i&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="mi"&gt;0&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="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Looks reasonable, right? Get the timezone offset, create a &lt;code&gt;Time&lt;/code&gt; object, convert to UTC.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;Time.now.in_time_zone().formatted_offset&lt;/code&gt; gets the offset for &lt;strong&gt;right now&lt;/strong&gt;, then applies it to any date being converted.&lt;/p&gt;

&lt;h2&gt;Why this breaks&lt;/h2&gt;

&lt;p&gt;Run this in December (EST, UTC-5):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;date_to_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&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;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;:eastern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Gets -05:00 offset, but June 20 should be EDT (-04:00)&lt;/span&gt;
&lt;span class="c1"&gt;# Result: off by one hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run the same code in June (EDT, UTC-4):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;date_to_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&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;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;:eastern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Gets -04:00 offset, correct for June&lt;/span&gt;
&lt;span class="c1"&gt;# Result: works fine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Same input, different output depending on when you run it. Your tests pass in summer, fail in winter. Data syncs would occasionally miss records or pull wrong date ranges, depending on DST periods.&lt;/p&gt;

&lt;h2&gt;The fix&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date_to_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;TIMEZONE_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone_key&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;day&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="mi"&gt;0&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="nf"&gt;utc&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;ActiveSupport::TimeZone#local&lt;/code&gt; handles DST correctly for the specific date being converted. June dates always get EDT, January dates always get EST, regardless of when the code runs.&lt;/p&gt;

&lt;h2&gt;The test that exposed it&lt;/h2&gt;

&lt;p&gt;Before touching the implementation, I wrote a test to confirm my suspicion — and it failed immediately.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'produces consistent results regardless of system timezone'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Date&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;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&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;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="sx"&gt;%w[UTC Asia/Tokyo America/Los_Angeles]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_zone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date_to_utc&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;:eastern&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="n"&gt;expected&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 test runs the same conversion in UTC, Tokyo, and LA timezones. The old implementation would produce different results depending on system timezone and time of year.&lt;/p&gt;

&lt;h2&gt;Impact&lt;/h2&gt;

&lt;p&gt;We caught this before it caused visible production issues, but the potential impact for a financial data integration was significant: off-by-one-hour shifts during DST transitions could cause missed records in date-range queries and validation mismatches between systems.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Never use &lt;code&gt;Time.now&lt;/code&gt; for calculations on other dates. If you need timezone info for a specific date, use that date.&lt;/li&gt;
&lt;li&gt;Test with explicit timezone manipulation. Don&amp;#39;t rely on your system&amp;#39;s timezone matching production.&lt;/li&gt;
&lt;li&gt;DST transitions are sneaky. A bug that manifests only during certain months can survive code review and testing.&lt;/li&gt;
&lt;li&gt;Know your tools: &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html"&gt;&lt;code&gt;ActiveSupport::TimeZone&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2026-01-14:/blog/stop-using-datetime-in-2026-unless-you-work-for-unesco/</id>
    <title type="html">Stop using DateTime in 2026 (unless you work for UNESCO)</title>
    <published>2026-01-14T14:08:58Z</published>
    <updated>2026-01-14T14:08:58Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/stop-using-datetime-in-2026-unless-you-work-for-unesco/" type="text/html"/>
    <content type="html">&lt;h1&gt;Stop using DateTime in 2026 (unless you work for UNESCO)&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;/code&gt; has been considered deprecated in Ruby since 3.0. It&amp;#39;s 2026. Why are people still using it?&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;During a recent code review, we found this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;whatever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starts_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When asked why &lt;code&gt;DateTime&lt;/code&gt; instead of &lt;code&gt;Time&lt;/code&gt;, the response was: &amp;quot;&lt;code&gt;DateTime&lt;/code&gt; handles a wider range of dates.&amp;quot;&lt;/p&gt;

&lt;p&gt;That was partially true. In 2008. On 32-bit systems.&lt;/p&gt;

&lt;h2&gt;DateTime&amp;#39;s range advantage died in Ruby 1.9.2&lt;/h2&gt;

&lt;p&gt;Before Ruby 1.9.2 (released in 2010), Time was limited by the system&amp;#39;s &lt;code&gt;time_t&lt;/code&gt; type — typically 32-bit signed integer covering 1901-2038. &lt;code&gt;DateTime&lt;/code&gt; had a much wider range.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ruby-doc.org/core-2.1.9/Time.html"&gt;Ruby 1.9.2 changed this&lt;/a&gt;. Time started using a signed 63-bit integer representing nanoseconds since epoch, giving it a range of 1823-2116. For dates outside this range, &lt;code&gt;Time&lt;/code&gt; uses &lt;code&gt;Bignum&lt;/code&gt; or &lt;code&gt;Rational&lt;/code&gt; — slower, but it works.&lt;/p&gt;

&lt;p&gt;The practical range advantage is gone.&lt;/p&gt;

&lt;h2&gt;Remember Rails 4.2?&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://knowyourmeme.com/memes/pepperidge-farm-remembers"&gt;&lt;em&gt;Pepperidge Farm Remembers&lt;/em&gt;&lt;/a&gt;. 
Some time ago when upgrading Rails app from 4.2 to 5.0, the test suite fortunately failed. The culprit was surprising: &lt;code&gt;DateTime#utc&lt;/code&gt; changed its return type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.rubyonrails.org/v4.2/classes/DateTime.html#method-i-utc"&gt;Rails 4.2&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; DateTime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://api.rubyonrails.org/v5.0/classes/DateTime.html#method-i-utc"&gt;Rails 5.0&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; Time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This broke several &lt;code&gt;Dry::Struct&lt;/code&gt; objects with strict type definitions expecting &lt;code&gt;DateTime&lt;/code&gt;. But instead of &amp;quot;fixing&amp;quot; the types, we asked a better question: &lt;em&gt;why were we using &lt;code&gt;DateTime&lt;/code&gt; at all?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rails 5&amp;#39;s breaking change to &lt;code&gt;DateTime#utc&lt;/code&gt; wasn&amp;#39;t a bug — it was a nudge. It was telling you: stop using this class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arkency.com/ruby-on-rails-upgrades/"&gt;Struggling with upgrades? We have a solution for you&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;The UNESCO problem&lt;/h2&gt;

&lt;p&gt;There&amp;#39;s actually &lt;strong&gt;one&lt;/strong&gt; legitimate use case for &lt;code&gt;DateTime&lt;/code&gt;: historical calendar reforms.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://ruby-doc.org/stdlib-2.4.1/libdoc/date/rdoc/DateTime.html"&gt;Ruby&amp;#39;s own documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It&amp;#39;s a common misconception that William Shakespeare and Miguel de Cervantes died on the same day in history - so much so that UNESCO named April 23 as World Book Day because of this fact. However, because England hadn&amp;#39;t yet adopted the Gregorian Calendar Reform (and wouldn&amp;#39;t until 1752) their deaths are actually 10 days apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ruby&amp;#39;s &lt;code&gt;Time&lt;/code&gt; uses a proleptic Gregorian calendar — it projects the Gregorian calendar backwards, ignoring historical reality. October 10, 1582 doesn&amp;#39;t exist in Italy (Pope Gregory XIII removed 10 days that October), but Ruby happily creates that timestamp.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;/code&gt; can handle different calendar reform dates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;shakespeare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1616-04-23'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ENGLAND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cervantes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1616-04-23'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ITALY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shakespeare&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cervantes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 10 days apart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For cataloging historical artifacts or dealing with pre-1752 dates across different countries, &lt;code&gt;DateTime&lt;/code&gt; is your tool.&lt;/p&gt;

&lt;p&gt;For literally everything else — which is 99.99% of applications — it&amp;#39;s the wrong choice.&lt;/p&gt;

&lt;p&gt;Norbert Wójtowicz gave an excellent &lt;a href="https://www.youtube.com/watch?v=YiLlnsq2fJ4"&gt;talk about calendars at wroclove.rb&lt;/a&gt; covering exactly these issues.&lt;/p&gt;

&lt;h2&gt;&lt;code&gt;DateTime&lt;/code&gt;’s actual problems&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No timezone support&lt;/strong&gt;. DateTime doesn&amp;#39;t handle timezones.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;DateTime: 2026-01-14T13:00:00+00:00&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;# Why +00:00 when my system is CET (+01:00)?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Incompatible with Rails&lt;/strong&gt;. &lt;code&gt;ActiveSupport&lt;/code&gt; extends &lt;code&gt;Time&lt;/code&gt; with timezone support. &lt;code&gt;DateTime&lt;/code&gt;? Barely.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;   &lt;span class="c1"&gt;# ✅ Respects Rails.application.config.time_zone&lt;/span&gt;
&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;   &lt;span class="c1"&gt;# ❌ Uses system timezone, ignores Rails config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Confusing arithmetic&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;      &lt;span class="c1"&gt;# =&amp;gt; 1 second later&lt;/span&gt;
&lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; 1 day later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This has caused bugs. Many bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignores DST&lt;/strong&gt;. &lt;code&gt;DateTime&lt;/code&gt; doesn&amp;#39;t track daylight saving time. If you use &lt;code&gt;DateTime&lt;/code&gt; for anything involving timezones, you will have bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;. &lt;code&gt;Time&lt;/code&gt; is faster. Noticeably.&lt;/p&gt;

&lt;h2&gt;Why people still use it&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&amp;quot;We&amp;#39;ve always used it&amp;quot;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code was written in 2009. It&amp;#39;s 2026. Update it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&amp;quot;I need to store dates without time&amp;quot;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;Date&lt;/code&gt;. That&amp;#39;s what it&amp;#39;s for.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ Rails.application.config.time_zone aware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;The library I&amp;#39;m using returns DateTime&amp;quot;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Convert immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;legacy_gem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in_time_zone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;What to use instead&lt;/h2&gt;

&lt;p&gt;For timestamps: &lt;code&gt;Time.current&lt;/code&gt; or &lt;code&gt;Time.zone.now&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For dates: &lt;code&gt;Date.current&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For parsing: &lt;code&gt;Time.zone.parse(&amp;#39;2026-01-14 13:00:00&amp;#39;)&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;The only exception&lt;/h2&gt;

&lt;p&gt;If you&amp;#39;re cataloging historical documents, artifacts, or working with dates before calendar reforms in different countries, &lt;code&gt;DateTime&lt;/code&gt; is your tool. You need to track which calendar reform date applies.&lt;/p&gt;

&lt;p&gt;For everything else — modern applications, APIs, databases — use &lt;code&gt;Time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;/code&gt; is deprecated for a reason.&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-2.1.9/Time.html"&gt;Ruby Time documentation - Ruby 1.9.2 changes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/stdlib-2.4.1/libdoc/date/rdoc/DateTime.html"&gt;Ruby DateTime documentation - UNESCO calendar problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=YiLlnsq2fJ4"&gt;Norbert Wójtowicz - It&amp;#39;s About Time (wroclove.rb)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rubystyle.guide/#no-datetime"&gt;Ruby Style Guide: No DateTime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2025-12-22:/blog/rewrite-with-confidence-validating-business-rules-through-isolated-testing/</id>
    <title type="html">Rewrite with Confidence: Validating Business Rules Through Isolated Testing</title>
    <published>2025-12-22T20:04:19Z</published>
    <updated>2025-12-22T20:04:19Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/rewrite-with-confidence-validating-business-rules-through-isolated-testing/" type="text/html"/>
    <content type="html">&lt;h1&gt;Rewrite with Confidence: Validating Business Rules Through Isolated Testing&lt;/h1&gt;

&lt;p&gt;A few months back, our team at Arkency faced a challenge that many Rails developers might recognize. We needed to implement a new flow at &lt;a href="https://www.lemonade.com"&gt;Lemonade&lt;/a&gt; that would eventually replace a legacy process — but with three major constraints that couldn&amp;#39;t be compromised: user experience, cost efficiency, and avoiding technical debt.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;The stakes were high. Any discrepancies between systems would impact customers and potentially create legal issues in the insurance domain. We had just three months to understand, replicate, and improve a complex flow that had evolved organically over years. And we needed to break free from obsolete data structures while preserving essential business rules embedded in a codebase with over 1 million lines of code.&lt;/p&gt;

&lt;p&gt;Traditional approaches wouldn&amp;#39;t work. Full test coverage would take months we didn&amp;#39;t have. What we needed was a methodology to systematically identify, isolate, and verify each business rule independently of its implementation.&lt;/p&gt;

&lt;p&gt;We needed a way to rewrite with confidence.&lt;/p&gt;

&lt;h2&gt;The Context: Insurtech at Scale&lt;/h2&gt;

&lt;p&gt;If you had asked me three years ago if insurtech could be exciting, I would have probably laughed. But it can be.&lt;/p&gt;

&lt;p&gt;Lemonade is an innovative insurance company that hit $1 billion in premiums just 10 years after founding. It took other well-established insurance brands 40–60 years to reach that milestone. Even companies like Microsoft, Netflix, Salesforce, and Tesla needed more time to achieve that.&lt;/p&gt;

&lt;p&gt;&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;What a thrill for &lt;a href="https://twitter.com/Lemonade_Inc?ref_src=twsrc%5Etfw"&gt;@lemonade_inc&lt;/a&gt; to be in the 𝘛𝘳𝘦𝘴 𝘊𝘰𝘮𝘮𝘢𝘴 Club! I’m not particularly moved by a car “with doors that open like 𝘵𝘩𝘪𝘴 👐”, but I’m definitely exhilarated by the ride so far, and can’t wait for our &lt;a href="https://twitter.com/hashtag/Next10X?src=hash&amp;amp;ref_src=twsrc%5Etfw"&gt;#Next10X&lt;/a&gt;! 🙌🏻🚀🎉 &lt;a href="https://t.co/HKpgfyFO7Y"&gt;&lt;a href="https://t.co/HKpgfyFO7Y"&gt;https://t.co/HKpgfyFO7Y&lt;/a&gt;&lt;/a&gt;&lt;/p&gt;&amp;mdash; Daniel Schreiber (@daschreiber) &lt;a href="https://twitter.com/daschreiber/status/1904512746571345965?ref_src=twsrc%5Etfw"&gt;March 25, 2025&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;When we joined Lemonade three years ago, their Director of Engineering shared a story that perfectly illustrated the stakes of our work. They once had an issue with roof coverage in one of their product lines and had to hire a legal team for a six-month sprint to fix things. The legal costs exceeded the entire IT budget.&lt;/p&gt;

&lt;p&gt;We couldn&amp;#39;t break things. We had to be 100% sure that the new flow provided the same outcome.&lt;/p&gt;

&lt;h2&gt;The Architecture: A Rails Monolith Under Transformation&lt;/h2&gt;

&lt;p&gt;Lemonade used a Rails monolith as their foundation — my favorite architecture. There&amp;#39;s no coincidence they became successful. Over the past few years, they&amp;#39;ve been transforming to a microservices architecture, with new product lines released using their internal framework. But all home and renters insurance is still handled within the Rails monolith.&lt;/p&gt;

&lt;p&gt;Our scope was clear: implement a new quote flow for HO4 (renters insurance) in the US that would produce identical underwriting results to the legacy system.&lt;/p&gt;

&lt;h2&gt;Understanding the Problem&lt;/h2&gt;

&lt;h3&gt;The God Model&lt;/h3&gt;

&lt;p&gt;Like many mature Rails applications, the system had a God model — &lt;code&gt;Quote&lt;/code&gt; — that accumulated responsibilities over time:&lt;/p&gt;
&lt;div class="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;Quote&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;serialize&lt;/span&gt; &lt;span class="ss"&gt;:data&lt;/span&gt;
  &lt;span class="n"&gt;serialize&lt;/span&gt; &lt;span class="ss"&gt;:answers&lt;/span&gt;

  &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="ss"&gt;pending: &lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;stubbed: &lt;/span&gt;&lt;span class="s1"&gt;'stubbed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;bindable: &lt;/span&gt;&lt;span class="s1"&gt;'bindable'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;uw_declined: &lt;/span&gt;&lt;span class="s1"&gt;'uw_declined'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s1"&gt;'pending'&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 business raised a valid point: &amp;quot;We don&amp;#39;t want &lt;code&gt;pending&lt;/code&gt; and &lt;code&gt;stubbed&lt;/code&gt; Quotes in the system.&amp;quot; Pending represented abandoned quotes with no value. Stubbed meant the system couldn&amp;#39;t make a risk assessment, usually due to third-party issues. This data model pollution required filtering at different levels:&lt;/p&gt;
&lt;div class="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;Quote&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:not_pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
    &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: :pending&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;We all have such excluding scopes in our apps — don&amp;#39;t pretend you don&amp;#39;t.&lt;/p&gt;

&lt;p&gt;This was especially problematic for the Data Science team. Without filtering these quotes, their models would be far from accurate.&lt;/p&gt;

&lt;h3&gt;The Data Complexity&lt;/h3&gt;

&lt;p&gt;The Quote model contained two serialized columns with deeply nested data. Here&amp;#39;s just a glimpse of what we were dealing with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;:locale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;:region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;US&lt;/span&gt;
    &lt;span class="na"&gt;:language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
  &lt;span class="na"&gt;:client_uuid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30a1377e-5f06-4e6f-878b-41564c2e1221&lt;/span&gt;
  &lt;span class="na"&gt;:user_logged_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;:flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;:send_pdf_sample_docs&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;:tenant_pet_damage_activated&lt;/span&gt;
  &lt;span class="c1"&gt;# ... and dozens more attributes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;answers&lt;/code&gt; hash was even more complex, containing everything from address components to user preferences to tracking data.&lt;/p&gt;

&lt;h3&gt;Why Traditional Approaches Failed&lt;/h3&gt;

&lt;p&gt;We initially tried static analysis to figure out which Quote attributes were necessary for the underwriting process. We quickly realized this was impossible — too many branches in the code. Imagine: every US state has its own regulations affecting insurance products. Multiply this by product editions that change over time due to legal concerns or business needs. We also share the data model and flow with home insurance.&lt;/p&gt;

&lt;p&gt;Then we tried using &lt;code&gt;Module#prepend&lt;/code&gt; to instrument Quote accessors and track which data was involved. This gave us better overview but was still overwhelming.&lt;/p&gt;

&lt;p&gt;And we hadn&amp;#39;t even touched the HTTP communication part — all the first-party and third-party calls required for underwriting, coverage selection, deductible calculation, and premium determination.&lt;/p&gt;

&lt;h3&gt;What About Other Approaches?&lt;/h3&gt;

&lt;p&gt;We considered several alternatives before settling on our solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shadow traffic&lt;/strong&gt; was an interesting option. This technique involves routing live production traffic to both the existing backend and a new shadow backend simultaneously. The shadow backend processes requests without affecting users, while comparison mechanisms validate behavior. Tools like &lt;a href="https://nginx.org/en/docs/http/ngx_http_mirror_module.html"&gt;nginx plugins&lt;/a&gt; or &lt;a href="https://github.com/zalando/skipper"&gt;Zalando&amp;#39;s Skipper&lt;/a&gt; can handle this elegantly.&lt;/p&gt;

&lt;p&gt;However, shadow traffic came with significant drawbacks for our use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Substantial infrastructure work and ongoing costs&lt;/li&gt;
&lt;li&gt;Potential compliance issues using production data in non-production environments&lt;/li&gt;
&lt;li&gt;Need to implement complex comparison mechanisms&lt;/li&gt;
&lt;li&gt;Difficulty avoiding side effects when dealing with stateful operations and third-party APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure overhead alone would have consumed a significant portion of our three-month deadline.&lt;/p&gt;

&lt;h2&gt;The Solution: Testing on Production&lt;/h2&gt;

&lt;p&gt;Here&amp;#39;s where we took an unconventional approach. Instead of trying to replicate production conditions in a test environment, we decided to test directly on production — but safely.&lt;/p&gt;

&lt;h3&gt;The Brave New Flow&lt;/h3&gt;

&lt;p&gt;The key architectural change was simple but profound: instead of creating a Quote at the beginning of the flow and updating it on every step, we&amp;#39;d receive all the data gathered by the frontend client and perform our task at the very end.&lt;/p&gt;

&lt;p&gt;This meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No more &lt;code&gt;pending&lt;/code&gt; quotes&lt;/li&gt;
&lt;li&gt;No more &lt;code&gt;stubbed&lt;/code&gt; quotes&lt;br&gt;&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;bindable&lt;/code&gt; or &lt;code&gt;uw_declined&lt;/code&gt; as final states&lt;/li&gt;
&lt;li&gt;Much cleaner data model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Implementing the Sampling Mechanism&lt;/h3&gt;

&lt;p&gt;We built a sampling system using Ruby&amp;#39;s &lt;code&gt;prepend&lt;/code&gt; to non-invasively inject our verification code:&lt;/p&gt;
&lt;div class="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;RentersUsQuoteSampler&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AroundFilter&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_prepare_for_preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;RentersUsQuoteSampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conditions_met_for_sampling?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;RentersUsQuoteSampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sampled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This allowed us to intercept the underwriting process for specific quotes without affecting the normal flow.&lt;/p&gt;

&lt;h3&gt;What We Sampled&lt;/h3&gt;

&lt;p&gt;For each qualifying quote, we captured:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Quote state before underwriting&lt;/strong&gt; - The raw quote data as it entered the process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quote state after underwriting&lt;/strong&gt; - The complete quote with pricing, deductible, and coverage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Address data&lt;/strong&gt; - All location information&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP interactions&lt;/strong&gt; - Every external API call made during the process&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="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;RentersUsQuoteSampler&lt;/span&gt;  
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sampled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lemonade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;quote_id: &lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before_quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;TyphoeusRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_recording&lt;/span&gt;
    &lt;span class="k"&gt;begin&lt;/span&gt;
      &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;
      &lt;span class="n"&gt;typhoeus_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TyphoeusRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recorded_requests&lt;/span&gt;
    &lt;span class="k"&gt;ensure&lt;/span&gt;
      &lt;span class="no"&gt;TyphoeusRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_recording&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="no"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;quote_before: &lt;/span&gt;&lt;span class="n"&gt;before_quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;address: &lt;/span&gt;&lt;span class="n"&gt;to_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;quote_after: &lt;/span&gt;&lt;span class="n"&gt;to_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;typhoeus_requests: &lt;/span&gt;&lt;span class="n"&gt;typhoeus_requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&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="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;hint: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;ignore_exclusions: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Recording HTTP Interactions&lt;/h3&gt;

&lt;p&gt;Lemonade used Typhoeus as the HTTP client for microservices and third-party communication. Fortunately, Typhoeus provides a callback system:&lt;/p&gt;
&lt;div class="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;RentersUsQuoteSampler&lt;/span&gt;  
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TyphoeusRecorder&lt;/span&gt;
    &lt;span class="no"&gt;RECORD_PROC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="vc"&gt;@@typhoeus_requests&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;serialize_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_recording&lt;/span&gt;
      &lt;span class="vc"&gt;@@typhoeus_requests&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="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;RECORD_PROC&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_recording&lt;/span&gt;
      &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_complete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RECORD_PROC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vc"&gt;@@typhoeus_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serialize_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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="ss"&gt;base_url: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:params&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;method: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:method&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
          &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;response&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="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 gave us perfect request-response pairs to use as stubs during verification.&lt;/p&gt;

&lt;h2&gt;The Verification Process&lt;/h2&gt;

&lt;p&gt;Sampling and verification were separate processes, allowing us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect samples from production continuously&lt;/li&gt;
&lt;li&gt;Run verification asynchronously&lt;/li&gt;
&lt;li&gt;Re-run verification after code fixes&lt;/li&gt;
&lt;li&gt;Iterate until we achieved parity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Leaving No Trace&lt;/h3&gt;

&lt;p&gt;The critical requirement was not polluting production with duplicate quotes:&lt;/p&gt;
&lt;div class="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;RentersUsQuoteDtoVerifier&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_rollback&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt;
      &lt;span class="k"&gt;raise&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;Rollback&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;But there was a gotcha: background jobs. We needed to ensure no jobs were scheduled within our rolled-back transaction.&lt;/p&gt;

&lt;h3&gt;After Commit Handling&lt;/h3&gt;

&lt;p&gt;This feature became built-in to Rails 7.2, but we weren&amp;#39;t there yet. Fortunately, one of the best things about working at Arkency is that if you need a solution, there&amp;#39;s a good chance we&amp;#39;ve solved it before — like in &lt;a href="https://github.com/RailsEventStore/rails_event_store/blob/92e0f920f7c11707ffe1c06f3e855827221fb77c/rails_event_store/lib/rails_event_store/after_commit_async_dispatcher.rb#L4"&gt;RailsEventStore&lt;/a&gt; or &lt;a href="https://blog.arkency.com/2015/10/run-it-in-background-job-after-commit/"&gt;our blog posts from 9 years before Rails introduced it&lt;/a&gt;.&lt;/p&gt;
&lt;div class="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;AfterCommitRunner&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;schedule_proc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_transaction&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinable?&lt;/span&gt;
      &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedule_proc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;schedule_proc&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedule_proc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;AsyncRecord&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;schedule_proc&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;class&lt;/span&gt; &lt;span class="nc"&gt;AsyncRecord&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;schedule_proc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@schedule_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schedule_proc&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;committed!&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="n"&gt;schedule_proc&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;def&lt;/span&gt; &lt;span class="nf"&gt;rolledback!&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;nil&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;before_committed!&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

    &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:schedule_proc&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 allowed us to queue jobs only after successful commits, not within rolled-back transactions.&lt;/p&gt;

&lt;h3&gt;HTTP Stubbing Strategy&lt;/h3&gt;

&lt;p&gt;We needed to stub all external HTTP calls to avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mutating state in other microservices&lt;/li&gt;
&lt;li&gt;Making expensive third-party API calls&lt;/li&gt;
&lt;li&gt;Affecting external systems (like credit scores)&lt;/li&gt;
&lt;li&gt;Rate limiting issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, we blocked all Typhoeus requests:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_http_stubs_mechanism&lt;/span&gt;
  &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;block_connection&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;req&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Expectation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then we used our recorded requests as stubs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_common_http_stubs&lt;/span&gt;
  &lt;span class="n"&gt;http_stubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;Typhoeus&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:base_url&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:params&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Typhoeus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Response&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;res&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;yield&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Handling Edge Cases&lt;/h3&gt;

&lt;p&gt;Some libraries used &lt;code&gt;net/http&lt;/code&gt; directly, which wasn&amp;#39;t easy to stub. For AWS S3 clients, we used Ruby&amp;#39;s metaprogramming capabilities:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_no_verisk_persistence&lt;/span&gt;
  &lt;span class="n"&gt;old_const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IamS3Resource&lt;/span&gt;
  &lt;span class="n"&gt;no_writes_iam_resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="n"&gt;old_const&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&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="s1"&gt;'http://example.org'&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presigned_url&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="s1"&gt;'http://example.org'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:remove_const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:IamS3Resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:IamS3Resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;no_writes_iam_resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="no"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:remove_const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:IamS3Resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:IamS3Resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_const&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This allowed us to override behavior while still downloading resources from S3 (assuming GETs don&amp;#39;t mutate state).&lt;/p&gt;

&lt;h3&gt;The Complete Verification Flow&lt;/h3&gt;

&lt;p&gt;Putting it all together:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_remake&lt;/span&gt;
  &lt;span class="n"&gt;sample_remake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

  &lt;span class="n"&gt;with_rollback&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;with_http_stubs_mechanism&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;with_common_http_stubs&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;with_bouncer_stubs&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="n"&gt;with_census_block_stubs&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="n"&gt;with_no_segment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
              &lt;span class="n"&gt;with_no_verisk_persistence&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="n"&gt;with_no_promises&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                  &lt;span class="n"&gt;with_no_impressions&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                    &lt;span class="n"&gt;remake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mk_quote&lt;/span&gt;
                    &lt;span class="no"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_prepare_for_preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remake&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;sample_remake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RentersUsQuoteSampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remake&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;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;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;span class="n"&gt;sample_remake&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Yes, the nesting looks deep, but each wrapper handled a specific concern. We could experiment safely as many times as needed.&lt;/p&gt;

&lt;h3&gt;Comparing Results&lt;/h3&gt;

&lt;p&gt;We used the &lt;code&gt;super_diff&lt;/code&gt; gem to identify discrepancies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt; 
  &lt;span class="n"&gt;tuple_to_compare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&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;def&lt;/span&gt; &lt;span class="nf"&gt;diff&lt;/span&gt;
  &lt;span class="no"&gt;SuperDiff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tuple_to_compare&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;Example output when things didn&amp;#39;t match:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;  &lt;span class="s2"&gt;"status"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"bindable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="s2"&gt;"status"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s2"&gt;"product"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"iso"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s2"&gt;"form"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"ho4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;  &lt;span class="s2"&gt;"edition"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"E240716"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="s2"&gt;"edition"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"E240618"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This worked beautifully for nested structures, which was crucial for our case.&lt;/p&gt;

&lt;h2&gt;The Results&lt;/h2&gt;

&lt;p&gt;After implementing this methodology, we achieved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fewer questions asked&lt;/strong&gt; - Simplified the customer flow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner data model&lt;/strong&gt; - Eliminated obsolete quote states&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identical outcomes&lt;/strong&gt; - 100% parity with legacy underwriting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence to ship&lt;/strong&gt; - No surprises in production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project leader shared across the organization:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&amp;quot;This is part of one of the best releases I have ever experienced.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There was even a panic moment when he reached out on a Friday evening before both our ski vacations — but it was just to thank the team for the exceptional release quality.&lt;/p&gt;

&lt;h2&gt;Key Takeaways&lt;/h2&gt;

&lt;p&gt;This approach worked because we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Separated collection from verification&lt;/strong&gt; — Continuous sampling with async verification&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treated production as the specification&lt;/strong&gt; — No need to replicate complex environments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated tests from side effects&lt;/strong&gt; — Transaction rollbacks and HTTP stubbing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterated until perfect&lt;/strong&gt; — Fixed issues and re-verified until parity achieved&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leveraged Ruby&amp;#39;s strengths&lt;/strong&gt; — Metaprogramming made complex stubbing manageable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The methodology is applicable beyond insurance or quote systems. Anytime you need to rewrite complex business logic while ensuring behavioral parity, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you sample real production behavior?&lt;/li&gt;
&lt;li&gt;Can you replay it safely in isolation?&lt;/li&gt;
&lt;li&gt;Can you compare results programmatically?&lt;/li&gt;
&lt;li&gt;Can you iterate until perfect?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When refactoring mission-critical business logic, traditional testing might not be enough. Sometimes the best test suite is production itself — as long as you can verify without breaking things.&lt;/p&gt;

&lt;h2&gt;Prefer watching?&lt;/h2&gt;

&lt;p&gt;This post is based on author’s conference talk delivered at &lt;a href="https://2025.wrocloverb.com"&gt;wroclove.rb 2025 in Wrocław, Poland&lt;/a&gt; and &lt;a href="2025.euruko.org"&gt;EuRuKo 2025 in Viana do Castelo, Portugal&lt;/a&gt;.&lt;/p&gt;

&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/OnoOHE6qFX4?si=busQb8WPl1j-CCUz" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;hr&gt;

&lt;p&gt;&lt;em&gt;This methodology emerged from real-world necessity at &lt;a href="https://clutch.co/go-to-review/deb08080-1847-4a21-af3b-1e92009311cd/365955"&gt;Lemonade&lt;/a&gt;. We&amp;#39;re grateful for their trust in letting us solve this challenge and share the solution with the Ruby community.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2025-10-30:/blog/the-joy-of-a-single-purpose-class-from-string-mutation-to-message-composition/</id>
    <title type="html">The Joy of a Single-Purpose Class: From String Mutation to Message Composition</title>
    <published>2025-10-30T14:24:50Z</published>
    <updated>2025-10-30T14:24:50Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/the-joy-of-a-single-purpose-class-from-string-mutation-to-message-composition/" type="text/html"/>
    <content type="html">&lt;h1&gt;The Joy of a Single-Purpose Class: From String Mutation to Message Composition&lt;/h1&gt;

&lt;p&gt;Recently I started the process of upgrading rather big Rails application to latest Ruby 3.4. I noticed a lot of warnings related to string literal mutation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="ss"&gt;warning: &lt;/span&gt;&lt;span class="n"&gt;literal&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;frozen&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;literal&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- more --&gt;

&lt;h2&gt;Ruby has both mutable and immutable strings&lt;/h2&gt;

&lt;p&gt;Let&amp;#39;s read &lt;a href="https://gist.github.com/fxn/bf4eed2505c76f4fca03ab48c43adc72#ruby-34"&gt;fxn&amp;#39;s explanation on this&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Ruby 3.4, by default, if a file does not have the magic comment and a string object that was instantiated with a literal gets mutated, Ruby still allows the mutation, but it now issues a warning&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was able to notice this early since my colleague &lt;a href="https://blog.arkency.com/authors/piotr-jurewicz/"&gt;Piotr&lt;/a&gt; took care about &lt;a href="https://blog.arkency.com/do-you-tune-out-ruby-deprecation-warnings/"&gt;not tuning out the Ruby deprecation warnings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article won’t be about benefits of freezing string literals, but if you’re curious about this topic, you should read this &lt;a href="https://gist.github.com/fxn/bf4eed2505c76f4fca03ab48c43adc72"&gt;gist by fxn&lt;/a&gt; with care and the article about &lt;a href="https://byroot.github.io/ruby/performance/2025/10/28/string-literals.html"&gt;Past, Present and Future of Frozen String Literal by byroot&lt;/a&gt; if you want a deep dive into details.&lt;/p&gt;

&lt;h2&gt;Problem with string literal mutation in our code&lt;/h2&gt;

&lt;p&gt;I noticed that there’s noticeable amount of deprecation messages related to modifying future frozen string literals coming from one module. It was the one responsible for producing and delivering Slack messages related to customer support, billing, frauds, etc. All the things that improve day-to-day operations in a serious business. 100+ methods representing messages to be delivered to various channels.&lt;/p&gt;

&lt;p&gt;The messages were composed in a few ways:&lt;/p&gt;
&lt;div class="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;Slack&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Billing&lt;/span&gt;
    &lt;span class="no"&gt;BILLING_CHANNEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

    &lt;span class="kp"&gt;extend&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;invoice_sent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;':postbox: *Invoice sent to customer*'&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;" | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;" | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer_email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;" | &amp;lt;&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;inovice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&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;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;"&lt;/span&gt;

      &lt;span class="n"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BILLING_CHANNEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payment_received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payment_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; Invoice: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoice_number&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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; Customer: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer_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="n"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BILLING_CHANNEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

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

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payment_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;':moneybag: *Payment Received*'&lt;/span&gt;
      &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;" | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;format_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&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;text&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;" | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="n"&gt;text&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;format_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;number_to_currency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locale: &lt;/span&gt;&lt;span class="n"&gt;locale&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;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&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;deliver_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;channel: &lt;/span&gt;&lt;span class="n"&gt;channel_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="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;Respective messages produced would be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:postbox: | *Inovice sent to customer* | Jane Doh | jan.doh@example.com | &amp;lt;https://fancyurl.example.com|KAKADUDU123&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:moneybag: *Payment Received* | $123.45 | Credit card
Invoice: KAKADUDU123
Customer: Jane Doh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Noticing the pattern&lt;/h2&gt;

&lt;p&gt;After reviewing around 100 methods delivering different messages, I instantly noticed the pattern and thought: &lt;em&gt;Ok, I can deal with that easily with a help of &lt;code&gt;Array&lt;/code&gt; and improve this repeatable, manual text decorations like &lt;code&gt;&amp;quot; | &amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;\n&amp;quot;&lt;/code&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ # frozen_string_literal: true
+ 
&lt;/span&gt; module Slack
   module Billing
     BILLING_CHANNEL_NAME = 'billing'
&lt;span class="err"&gt;
&lt;/span&gt;     extend self
&lt;span class="err"&gt;
&lt;/span&gt;     def invoice_sent(invoice)
&lt;span class="gd"&gt;-      message = ':postbox: *Invoice sent to customer*'
-      message &amp;lt;&amp;lt; " | #{invoice.customer_name}"
-      message &amp;lt;&amp;lt; " | #{invoice.customer_email}"
-      message &amp;lt;&amp;lt; " | &amp;lt;#{inovice.url}|#{invoice.number}"
&lt;/span&gt;&lt;span class="gi"&gt;+      message = [':postbox: *Invoice sent to customer*']
+      message &amp;lt;&amp;lt; invoice.customer_name
+      message &amp;lt;&amp;lt; invoice.customer_email
+      message &amp;lt;&amp;lt; "&amp;lt;#{inovice.url}|#{invoice.number}&amp;gt;"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      send_message(BILLING_CHANNEL_NAME, message)
&lt;/span&gt;&lt;span class="gi"&gt;+      send_message(BILLING_CHANNEL_NAME, message.join(" | "))
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     def payment_received(payment, locale)
&lt;span class="gd"&gt;-      message = payment_text(payment, locale)
-      message.push("\n Invoice: #{payment.invoice_number}")
-      message.push("\n Customer: #{payment.customer_name}")
&lt;/span&gt;&lt;span class="gi"&gt;+      message = [payment_text(payment, locale)]
+      message.push("Invoice: #{payment.invoice_number}")
+      message.push("Customer: #{payment.customer_name}")
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      send_message(BILLING_CHANNEL_NAME, messsage)
&lt;/span&gt;&lt;span class="gi"&gt;+      send_message(BILLING_CHANNEL_NAME, message.join("\n")) 
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     private
&lt;span class="err"&gt;
&lt;/span&gt;     def payment_text(payment, locale)
&lt;span class="gd"&gt;-      text = ':moneybag: *Payment Received*'
-      text &amp;lt;&amp;lt; " | #{format_amount(payment.amount, locale)}"
-      text &amp;lt;&amp;lt; " | #{payment.channel}"
&lt;/span&gt;&lt;span class="gi"&gt;+      text = [':moneybag: *Payment Received*']
+      text &amp;lt;&amp;lt; format_amount(payment.amount)
+      text &amp;lt;&amp;lt; payment.channel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      text
&lt;/span&gt;&lt;span class="gi"&gt;+      text.join(" | ")
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     def format_amount(amount, locale)
       number_to_currency(amount, locale: locale)
     end
&lt;span class="err"&gt;
&lt;/span&gt;     def send_message(channel_name, message)
       Slack::Client.deliver_message(channel: channel_name, message: message)
     end
   end
 end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What we’ve gained by this refactoring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No string literal mutation, so there will be no warnings on Ruby 3.4 and potential issues in the future&lt;/li&gt;
&lt;li&gt;Less repeatable code, no artisanal text delimiter crafting&lt;/li&gt;
&lt;li&gt;We still used the same methods for composing message as both &lt;code&gt;String&lt;/code&gt; and &lt;code&gt;Array&lt;/code&gt; provide &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;push&lt;/code&gt; methods. I wanted to keep this code similar to previous implementation without any radical changes so other maintainers would be instantly familiarized with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Improve the code&lt;/h2&gt;

&lt;p&gt;I’m sick of primitive obsession in the codebase. I don’t like all those &lt;code&gt;Array&lt;/code&gt; related internals exposed, irrelevant in the context of building a message. We operate on a very simple example here, multiply this 50 times, add even more complex methods to that. &lt;/p&gt;

&lt;p&gt;What if we introduced dedicated object which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;produces strings in an immutable manner&lt;/li&gt;
&lt;li&gt;hides all the separator plumbing as most of the 100+ messages use &lt;code&gt;|&lt;/code&gt; to separate message parts&lt;/li&gt;
&lt;li&gt;deals with empty strings&lt;/li&gt;
&lt;li&gt;has API similar to current implementation&lt;/li&gt;
&lt;li&gt;allows composition like current implementation &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at the implementation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Slack&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;
    &lt;span class="no"&gt;DELIMITER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;' | '&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="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delimiter: &lt;/span&gt;&lt;span class="no"&gt;DELIMITER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@delimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;
      &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&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;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;message_part&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact_blank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@delimiter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kp"&gt;alias_method&lt;/span&gt; &lt;span class="ss"&gt;:to_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:to_s&lt;/span&gt;
    &lt;span class="kp"&gt;alias_method&lt;/span&gt; &lt;span class="ss"&gt;:push&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:&amp;lt;&amp;lt;&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;h3&gt;Benefits of ActiveSupport&lt;/h3&gt;

&lt;p&gt;What’s important to notice is the fact that it’s a Rails and we benefit from ActiveSupport` here, specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.rubyonrails.org/classes/Enumerable.html#method-i-compact_blank"&gt;&lt;code&gt;compact_blank&lt;/code&gt;&lt;/a&gt; in an explicit manner&lt;/li&gt;
&lt;li&gt;and &lt;a href="https://blog.arkency.com/2017/07/nil-empty-blank-ruby-rails-difference/#_code_blank___code_"&gt;&lt;code&gt;blank?&lt;/code&gt;&lt;/a&gt; in an implicit way as &lt;code&gt;Object&lt;/code&gt; extension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise we would need to put a bit more effort into our class:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="vi"&gt;@message&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:empty?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@delimiter&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;It could be split into private method, but I like the explicitness &lt;code&gt;compact_blank&lt;/code&gt; provides and I’m fine with using it.&lt;/p&gt;

&lt;h3&gt;Pass a single string or multiple as an argument&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;*parts&lt;/code&gt; parameter leverages Ruby&amp;#39;s splat operator to automatically collect all positional arguments into an array under &lt;code&gt;@messages&lt;/code&gt;. This gives us a flexible constructor without forcing callers to wrap arguments in array literals.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka"&lt;/span&gt;

&lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dudu'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka | dudu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Append our message using different methods&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'dudu'&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka | dudu"&lt;/span&gt;

&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt; &lt;span class="s1"&gt;'dudu'&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka | dudu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Default delimiter, but still customizable&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dudu'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka | dudu"&lt;/span&gt;

&lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dudu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delimiter: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;dudu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Compose various &lt;code&gt;Slack::Message&lt;/code&gt; object with different delimiters&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'dudu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delimiter: &lt;/span&gt;&lt;span class="s2"&gt;" — "&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka | dudu — foo"&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'kaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delimiter: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Message&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="s1"&gt;'dudu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delimiter: &lt;/span&gt;&lt;span class="s1"&gt;' ~ '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"kaka&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;dudu ~ foo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The magic happens through the &lt;code&gt;to_str&lt;/code&gt; alias. When  &lt;code&gt;@message.join(@delimiter)&lt;/code&gt; is called, Ruby&amp;#39;s &lt;code&gt;Array#join&lt;/code&gt; implicitly calls &lt;code&gt;to_str&lt;/code&gt; on each element (it would fallback to &lt;code&gt;to_s&lt;/code&gt; if not defined). Since &lt;code&gt;to_str&lt;/code&gt; is aliased to &lt;code&gt;to_s&lt;/code&gt;, nested &lt;code&gt;Slack::Message&lt;/code&gt; objects get automatically stringified.&lt;/p&gt;

&lt;p&gt;This recursive flattening happens transparently because &lt;code&gt;to_str&lt;/code&gt; signals to Ruby that our &lt;a href="https://ruby-doc.org/3.4/implicit_conversion_rdoc.html#label-String-Convertible+Objects"&gt;object can be treated as a string in implicit contexts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;Final refactoring&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt; # frozen_string_literal: true
&lt;span class="err"&gt;
&lt;/span&gt; module Slack
   module Billing
     BILLING_CHANNEL_NAME = 'billing'
&lt;span class="err"&gt;
&lt;/span&gt;     extend self
&lt;span class="err"&gt;
&lt;/span&gt;     def invoice_sent(invoice)
&lt;span class="gd"&gt;-      message = [':postbox: *Invoice sent to customer*']
-      message &amp;lt;&amp;lt; invoice.customer_name
-      message &amp;lt;&amp;lt; invoice.customer_email
-      message &amp;lt;&amp;lt; "&amp;lt;#{inovice.url}|#{invoice.number}&amp;gt;"
&lt;/span&gt;&lt;span class="gi"&gt;+      message = Message.new(':postbox: *Invoice sent to customer*')
+      message &amp;lt;&amp;lt; invoice.customer_name
+      message &amp;lt;&amp;lt; invoice.customer_email
+      message &amp;lt;&amp;lt; "&amp;lt;#{inovice.url}|#{invoice.number}"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      send_message(BILLING_CHANNEL_NAME, message.join(" | "))
&lt;/span&gt;&lt;span class="gi"&gt;+      send_message(BILLING_CHANNEL_NAME, message))
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     def payment_received(payment, locale)
&lt;span class="gd"&gt;-      message = [payment_text(payment, locale)]
-      message.push("Invoice: #{payment.invoice_number}")
-      message.push("Customer: #{payment.customer_name}")
&lt;/span&gt;&lt;span class="gi"&gt;+      message = Message.new(payment_text(payment, locale), delimiter: "\n")
+      message.push("Invoice: #{payment.invoice_number}")
+      message.push("Customer: #{payment.customer_name}")
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      send_message(BILLING_CHANNEL_NAME, messsage.join("\n"))
&lt;/span&gt;&lt;span class="gi"&gt;+      send_message(BILLING_CHANNEL_NAME, message)
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     private
&lt;span class="err"&gt;
&lt;/span&gt;     def payment_text(payment, locale)
&lt;span class="gd"&gt;-      text = [':moneybag: *Payment Received*']
-      text &amp;lt;&amp;lt; format_amount(payment.amount, locale)
-      text &amp;lt;&amp;lt; payment.channel
&lt;/span&gt;&lt;span class="gi"&gt;+      text = Message.new(':moneybag: *Payment Received*')
+      text &amp;lt;&amp;lt; format_amount(payment.amount, locale)
+      text &amp;lt;&amp;lt; payment.channel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      text.join(" | ")
&lt;/span&gt;&lt;span class="gi"&gt;+      text
&lt;/span&gt;     end
&lt;span class="err"&gt;
&lt;/span&gt;     def format_amount(amount, locale)
       number_to_currency(amount, locale: locale)
     end
&lt;span class="err"&gt;
&lt;/span&gt;     def send_message(channel_name, message)
&lt;span class="gd"&gt;-      Slack::Client.deliver_message(channel: channel_name, message: message)
&lt;/span&gt;&lt;span class="gi"&gt;+      Slack::Client.deliver_message(channel: channel_name, message: message.to_s)
&lt;/span&gt;     end
   end
 end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I can’t remember the last time I had so much joy introducing such a simple, single-purpose class. In such moments you rediscover the true beauty of Ruby.&lt;/p&gt;

&lt;h2&gt;Summary&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The class name &lt;code&gt;Message&lt;/code&gt; nicely reveals its intent. We’re composing some message here.&lt;/li&gt;
&lt;li&gt;There’s no need for artisanal delimiter orchestration&lt;/li&gt;
&lt;li&gt;Our object composes nicely from different pieces&lt;/li&gt;
&lt;li&gt;Output is predictable and immutable&lt;/li&gt;
&lt;li&gt;&lt;code&gt;frozen string literal&lt;/code&gt; warnings are gone&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2025-07-01:/blog/stop-concatenating-urls-with-strings/</id>
    <title type="html">Stop concatenating URLs with strings — Use proper tools instead</title>
    <published>2025-07-01T14:10:21Z</published>
    <updated>2025-07-01T14:10:21Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/stop-concatenating-urls-with-strings/" type="text/html"/>
    <content type="html">&lt;h1&gt;Stop concatenating URLs with strings — Use proper tools instead&lt;/h1&gt;

&lt;p&gt;How many times have you seen code like this in a Ruby application?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;
&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/users"&lt;/span&gt;
&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;full_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;endpoint&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;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At first glance, it looks harmless, but it hides several traps that can lead to hard–to–debug errors.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;h2&gt;Problems with Naive URL Concatenation&lt;/h2&gt;

&lt;h3&gt;1. Double or missing slashes&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com/"&lt;/span&gt; &lt;span class="c1"&gt;# has trailing hash&lt;/span&gt;
&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/users"&lt;/span&gt; &lt;span class="c1"&gt;# has leading hash&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;{#endpoint}"&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "https://api.example.com//users"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Double slash is likely not desired. While most servers will handle it, this looks unprofessional.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt; &lt;span class="c1"&gt;# no trailing hash&lt;/span&gt;
&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="c1"&gt;# no leading hash&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;{#endpoint}"&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "https://api.example.comusers"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;TLD concatenated with &lt;code&gt;path&lt;/code&gt; is not a thing we’re looking for.&lt;/p&gt;

&lt;h3&gt;2. Improper parameter escaping&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com/search?q=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "https://api.example.com/search?q=hello world"&lt;/span&gt;
&lt;span class="c1"&gt;# space is not properly encoded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. Protocol and port issues&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"localhost:3000"&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/users"&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What if host already contains protocol?&lt;/p&gt;

&lt;h2&gt;Solutions&lt;/h2&gt;

&lt;h3&gt;1. Ruby’s URI&lt;/h3&gt;

&lt;p&gt;Ruby has a built–in &lt;a href="https://docs.ruby-lang.org/en/master/URI.html"&gt;URI&lt;/a&gt; class that solves most of these problems:&lt;/p&gt;
&lt;div class="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;"uri"&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/users"&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_www_form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"hello world"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "https://api.example.com/users?q=hello+world&amp;amp;limit=10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let’s join some paths:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/api/v1/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;URI::HTTPS https://api.example.com/api/v1/users/123&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;More advanced URI example&lt;/h4&gt;
&lt;div class="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;ApiClient&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;base_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&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;build_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@base_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_www_form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ApiClient&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;"https://api.example.com/v1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"users/123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;include: &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; https://api.example.com/v1/users/123?include=profile&amp;amp;format=json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. Pathname for local paths&lt;/h3&gt;

&lt;p&gt;If you’re working with file paths or local URLs, &lt;code&gt;Pathname&lt;/code&gt; can be helpful:&lt;/p&gt;
&lt;div class="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;"pathname"&lt;/span&gt;

&lt;span class="n"&gt;base_bath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Pathname&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;"/var/www/uploads"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user_folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"user_123"&lt;/span&gt;
&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"avatar.jpg"&lt;/span&gt;

&lt;span class="n"&gt;full_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;Pathname:/var/www/uploads/user_123/avatar.jpg&amp;gt;&lt;/span&gt;

&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file://&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;full_path&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="c1"&gt;# =&amp;gt; #&amp;lt;URI::File file:///var/www/uploads/user_123/avatar.jpg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One more cool trick with pathname is &lt;code&gt;/&lt;/code&gt; method (alias for &lt;code&gt;+&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Pathname&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;"/var"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;"www"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;"uploads"&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;Pathname:/var/www/uploads&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. Addresable gem&lt;/h3&gt;

&lt;p&gt;For more advanced use cases, consider the &lt;a href="https://github.com/sporkmonger/addressable"&gt;addressable&lt;/a&gt; gem:&lt;/p&gt;
&lt;div class="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;"addressable/uri"&lt;/span&gt;

&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Addressable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;URI&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;scheme: &lt;/span&gt;&lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="s2"&gt;"users/123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;query_values: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;includes: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "https://api.example.com/users/123?includes[]=profile&amp;amp;includes[]=posts&amp;amp;format=json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4. Rails URL helpers&lt;/h3&gt;

&lt;p&gt;In Rails applications, use built–in helpers instead of string concatenation whenever possible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Instead of:&lt;/span&gt;
&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://myapp.com"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="si"&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;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/posts/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;post&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="c1"&gt;# Use this&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_post_url&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;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="s2"&gt;"https://myapp.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Or this&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_v1_user_url&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="ss"&gt;host: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;5. Test URL building&lt;/h3&gt;

&lt;p&gt;You might think that having test can catch these URL building issues and you would be partially right. However, many developers stub their HTTP client methods directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_client&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;receieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:get&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This test won’t catch URL formatting issues.&lt;/p&gt;

&lt;p&gt;When you stub the HTTP client method itself, malformed ULRs slip through because stub intercepts the call regardless of what URL was passed. A better approach is using &lt;a href="https://github.com/bblimke/webmock"&gt;WebMock&lt;/a&gt;, which sets expectations on the actual URLs being requested:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;stub_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com/users/123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;WebMock will fail if the URL is malformed&lt;/li&gt;
&lt;li&gt;This will catch URL building errors:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;double slashes&lt;/li&gt;
&lt;li&gt;missing slashes&lt;/li&gt;
&lt;li&gt;unescaped parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebMock forces you to be explicit about the exact URLs your code should generated, making URL building bugs much more visible during test runs.&lt;/p&gt;

&lt;h2&gt;Summary&lt;/h2&gt;

&lt;p&gt;Concatenating URLs with strings is a recipe for trouble. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;URI&lt;/code&gt; for basic URL building scenarios&lt;/li&gt;
&lt;li&gt;Leverage &lt;code&gt;Pathname&lt;/code&gt; for local file paths&lt;/li&gt;
&lt;li&gt;Consider &lt;code&gt;addressable&lt;/code&gt; for advanced use cases&lt;/li&gt;
&lt;li&gt;Utilize Rails URL helpers in Rails applications&lt;/li&gt;
&lt;li&gt;Always escape parameters using &lt;code&gt;URI.encode_www_form&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Test your external http calls &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your code will be more reliable, easier to maintain and less prone to URL formatting errors.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2025-04-04:/blog/installing-precompiled-native-gems-with-bundle-lock-add-platform/</id>
    <title type="html">Installing Precompiled Native Gems with bundle lock --add-platform</title>
    <published>2025-04-04T07:27:12Z</published>
    <updated>2025-04-04T07:27:12Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/installing-precompiled-native-gems-with-bundle-lock-add-platform/" type="text/html"/>
    <content type="html">&lt;h1&gt;Installing Precompiled Native Gems with bundle lock --add-platform&lt;/h1&gt;

&lt;p&gt;There&amp;#39;s a great chance that your Ruby app occasionally explodes during bundle install because of native extensions. There&amp;#39;s an even greater chance that it happens with &lt;code&gt;nokogiri&lt;/code&gt;, &lt;code&gt;ffi&lt;/code&gt; or some other notorious gem with C extensions. The problem gets worse when you&amp;#39;re working across different operating systems or upgrading Ruby versions. Let&amp;#39;s fix this once and for all.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;h2&gt;My problem with native gems&lt;/h2&gt;

&lt;p&gt;As we perform a ton of &lt;a href="https://arkency.com/ruby-on-rails-upgrades/"&gt;Ruby and Rails upgrades&lt;/a&gt; across different projects at arkency, we were struck by those issues many times.&lt;/p&gt;

&lt;p&gt;My main concern with native gems is that they create unnecessary friction in your development and deployment workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every single &lt;code&gt;bundle install&lt;/code&gt; takes ages if compilation has to occur&lt;/li&gt;
&lt;li&gt;Ruby version upgrades? Prepare to recompile everything&lt;/li&gt;
&lt;li&gt;Different OS than your teammates? Enjoy your unique set of errors&lt;/li&gt;
&lt;li&gt;CI pipeline running slow? Blame those C extensions&lt;/li&gt;
&lt;li&gt;YJIT performance gains are limited with C extensions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This last point is often overlooked. Ruby’s YJIT (Yet Another Just–In–Time compiler) can significantly speed up your application, but it works best with pure Ruby code. C extensions bypass the Ruby VM, which means YJIT can’t optimize them. The more your app relies on native extensions, the fewer benefits you’ll see from YJIT. With Ruby 3.3, YJIT can be enabled with the &lt;code&gt;--yjit&lt;/code&gt; flag, so you’re potentially missing out on free performance gains, but &lt;a href="https://github.com/rails/rails/pull/49947"&gt;Rails will do it for you&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Btw. here’s excellent &lt;a href="https://jpcamara.com/2024/12/01/speeding-up-ruby.html"&gt;article on speeding up Ruby by rewriting C... in Ruby&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s particularly frustrating on deployment. You’ve built a beautiful containerized setup, but still need to install build dependencies just to compile the same gems over and over. Your Docker images are bloated with compilers and dev headers that serve no purpose in production.&lt;/p&gt;

&lt;h2&gt;It’s not a new problem&lt;/h2&gt;

&lt;p&gt;What inspired me to share this solution with you is a &lt;a href="/rails-when-nothing-changed-is-the-best-feature/"&gt;recent chat with my friend&lt;/a&gt;, especially this part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recently I had to implement a tiny backend app. I dusted off Rails and everything was the same. Same commands, same gems, even nokogiri crashed the same way during bundle install like 10 years ago...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn’t have to be that way, I thought.&lt;/p&gt;

&lt;h2&gt;Bundler, the hero we need&lt;/h2&gt;

&lt;p&gt;Here it comes: &lt;code&gt;bundle lock --add-platform&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This command tells Bundler to resolve dependencies for platforms other than your current one and store that information in your &lt;code&gt;Gemfile.lock&lt;/code&gt;. When these platforms provide precompiled versions, Bundler will use them instead of trying to compile from source.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--add-platform&lt;/code&gt; option has been available since Bundler &lt;code&gt;2.2.0&lt;/code&gt;, so make sure you’re running a recent version . This can be easily checked with &lt;code&gt;bundle -v&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If for some reason your &lt;code&gt;Gemfile.lock&lt;/code&gt; is lacking &lt;code&gt;PLATFORMS&lt;/code&gt; section, e.g. you’re upgrading good’ol app, you should follow next steps.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;Gemfile.lock&lt;/code&gt; has &lt;code&gt;PLATFORMS&lt;/code&gt; present, but it’s lacking the specific platform you run your app on, you should follow my article.&lt;/p&gt;

&lt;h2&gt;What are the PLATFORMS?&lt;/h2&gt;

&lt;p&gt;The answer is simple and lives on your machine:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  gem help platform
RubyGems platforms are composed of three parts, a CPU, an OS, and a
version.  These values are taken from values in rbconfig.rb.  You can view
your current platform by running `gem environment`.

RubyGems matches platforms as follows:

  * The CPU must match exactly unless one of the platforms has
    "universal" as the CPU or the local CPU starts with "arm" and the gem's
    CPU is exactly "arm" (for gems that support generic ARM architecture).
  * The OS must match exactly.
  * The versions must match exactly unless one of the versions is nil.

For commands that install, uninstall and list gems, you can override what
RubyGems thinks your platform is with the --platform option.  The platform
you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}".  On mswin
platforms, the version is the compiler version, not the OS version.  (Ruby
compiled with VC6 uses "60" as the compiler version, VC8 uses "80".)

For the ARM architecture, gems with a platform of "arm-linux" should run on a
reasonable set of ARM CPUs and not depend on instructions present on a limited
subset of the architecture.  For example, the binary should run on platforms
armv5, armv6hf, armv6l, armv7, etc.  If you use the "arm-linux" platform
please test your gem on a variety of ARM hardware before release to ensure it
functions correctly.

Example platforms:

  x86-freebsd        # Any FreeBSD version on an x86 CPU
  universal-darwin-8 # Darwin 8 only gems that run on any CPU
  x86-mswin32-80     # Windows gems compiled with VC8
  armv7-linux        # Gem complied for an ARMv7 CPU running linux
  arm-linux          # Gem compiled for any ARM CPU running linux

When building platform gems, set the platform in the gem specification to
Gem::Platform::CURRENT.  This will correctly mark the gem with your ruby's
platform.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;

&lt;h3&gt;Modern tooling&lt;/h3&gt;

&lt;p&gt;Make sure you have recent Bundler and Rubygems, just to avoid hiccups and benefit from improvements:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem install bundler
gem update --system
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If new version of bundler has been installed, make sure to let your &lt;code&gt;Gemfile.lock&lt;/code&gt; to be aware of it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle update --bundler

git add Gemfile.lock
git commit -m "Updated bundler"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Precompiled gem versions available for your platform&lt;/h3&gt;

&lt;p&gt;I recommend going to &lt;a href="https://rubygems.org"&gt;Rubygems&lt;/a&gt; page and checking versions page of a desired gem, let’s use &lt;a href="https://rubygems.org/gems/nokogiri/versions"&gt;nokogiri&lt;/a&gt; as an example.&lt;/p&gt;

&lt;p&gt;For the day of writing this post, &lt;code&gt;1.18.7&lt;/code&gt; is the most recent version. You’ll see raw version (compiled on your machine):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.18.7 March 31, 2025 (4.16 MB)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;along with precompiled ones:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.18.7 March 31, 2025 x86_64-linux-gnu (3.88 MB)
1.18.7 March 31, 2025 arm-linux-gnu (3.25 MB)
1.18.7 March 31, 2025 aarch64-linux-gnu (3.8 MB)
1.18.7 March 31, 2025 arm-linux-musl (3.44 MB)
1.18.7 March 31, 2025 x86_64-linux-musl (3.87 MB)
1.18.7 March 31, 2025 arm64-darwin (6.23 MB)
1.18.7 March 31, 2025 x86_64-darwin (6.4 MB)
1.18.7 March 31, 2025 aarch64-linux-musl (3.77 MB)
1.18.7 March 31, 2025 java (9.88 MB)
1.18.7 March 31, 2025 x64-mingw-ucrt (6.02 MB)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Updated &lt;code&gt;Gemfile.lock&lt;/code&gt; with platform–specific dependencies&lt;/h3&gt;

&lt;p&gt;The rule of thumb for me is adding platforms below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle lock &lt;span class="nt"&gt;--add-platform&lt;/span&gt; arm64-darwin
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock &lt;span class="nt"&gt;--add-platform&lt;/span&gt; x86_64-darwin
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock &lt;span class="nt"&gt;--add-platform&lt;/span&gt; x86_64-linux
Writing lockfile to /Users/fidel/code/Gemfile.lock

bundle &lt;span class="nb"&gt;install

&lt;/span&gt;git add Gemfile.lock
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Use precompiled gems for all the platforms"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The example above covers the most common platforms for Rails development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intel/AMD Linux (most servers)&lt;/li&gt;
&lt;li&gt;Apple Silicon (M1/M2/M3/M4 and counting Macs)&lt;/li&gt;
&lt;li&gt;Intel Macs — as not everyone is running cutting–edge hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Obviously, you can add any other platform that you need.&lt;/p&gt;

&lt;p&gt;Enjoy no surprises during deployment or next Ruby upgrade.&lt;/p&gt;

&lt;h2&gt;Forget everything I’ve told you so far&lt;/h2&gt;

&lt;p&gt;If you’re running a modern Bundler, it will do the platform job for you:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  cat Gemfile
source "http://rubygems.org"

gem "nokogiri"

➜  bundle
Fetching gem metadata from http://rubygems.org/.......
Resolving dependencies...
Fetching nokogiri 1.18.7 (arm64-darwin)
Installing nokogiri 1.18.7 (arm64-darwin)
Bundle complete! 1 Gemfile dependency, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

➜  cat Gemfile.lock
GEM
  remote: http://rubygems.org/
  specs:
    nokogiri (1.18.7-aarch64-linux-gnu)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-aarch64-linux-musl)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-arm-linux-gnu)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-arm-linux-musl)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-arm64-darwin)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-x86_64-darwin)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-x86_64-linux-gnu)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-x86_64-linux-musl)
      racc (~&amp;gt; 1.4)
    racc (1.8.1)

PLATFORMS
  aarch64-linux-gnu
  aarch64-linux-musl
  arm-linux-gnu
  arm-linux-musl
  arm64-darwin
  x86_64-darwin
  x86_64-linux-gnu
  x86_64-linux-musl

DEPENDENCIES
  nokogiri

BUNDLED WITH
   2.6.5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;See, I didn’t even need to lock platforms manually. However, it added all the platforms this particular gem is available for. Except Windows and Java ones — coincidence? ;)&lt;/p&gt;

&lt;p&gt;But I think that less is more and I prefer keeping &lt;code&gt;Gemfile.lock&lt;/code&gt; as minimal as possible. And yes, there’s a command for that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle lock --remove-platform x86_64-linux-musl
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock --remove-platform aarch64-linux-gnu
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock --remove-platform aarch64-linux-musl
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock --remove-platform arm-linux-musl
Writing lockfile to /Users/fidel/code/Gemfile.lock
bundle lock --remove-platform arm-linux-gnu
Writing lockfile to /Users/fidel/code/Gemfile.lock

➜  cat Gemfile.lock
GEM
  remote: http://rubygems.org/
  specs:
    nokogiri (1.18.7-arm64-darwin)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-x86_64-darwin)
      racc (~&amp;gt; 1.4)
    nokogiri (1.18.7-x86_64-linux-gnu)
      racc (~&amp;gt; 1.4)
    racc (1.8.1)

PLATFORMS
  arm64-darwin
  x86_64-darwin
  x86_64-linux-gnu

DEPENDENCIES
  nokogiri

BUNDLED WITH
   2.6.5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It removed &lt;code&gt;PLATFORMS&lt;/code&gt; entries and sufficient gems specifications we aren’t planning on using.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ok, but why should I do that?&lt;/em&gt; — you might ask. And here’s the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you won’t be downloading obsolete gem versions on your CI or production deployment&lt;/li&gt;
&lt;li&gt;you save your bandwidth&lt;/li&gt;
&lt;li&gt;you make &lt;a href="https://rubygems.org"&gt;RubyGems.org&lt;/a&gt; happy by not pulling unnecessary stuff from their CDN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your fast and predictable builds 🖖&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2025-04-03:/blog/rails-when-nothing-changed-is-the-best-feature/</id>
    <title type="html">Rails: when "nothing changed" is the best feature</title>
    <published>2025-04-03T19:42:38Z</published>
    <updated>2025-04-03T19:42:38Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/rails-when-nothing-changed-is-the-best-feature/" type="text/html"/>
    <content type="html">&lt;h1&gt;Rails: when &amp;quot;nothing changed&amp;quot; is the best feature&lt;/h1&gt;

&lt;p&gt;Recently, I had a chat with a friend of mine, who used to do Rails back in the days. For the last ~10 years he’s focused on mobile development. I was curious what are his observations and asked if he’s happy with his decision or maybe he actually misses web development. &lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;He replied:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I miss doing backend, but I’m disgusted with web development. I’m too old to chase every new framework to do the same thing in every new ES flavor of JS... Jumping around npm, yarn, pnpm, bun or whatever is cool this quarter...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But then he followed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recently I had to implement a tiny backend app. I dusted off Rails and everything was the same. Same commands, same gems, even nokogiri crashed the same way during bundle install, just like 10 years ago...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For me it’s an impressive story about boring software. Boring software that’s highly regarded. I absolutely love Rails longevity.&lt;/p&gt;

&lt;h2&gt;Low learning curve for returnees&lt;/h2&gt;

&lt;p&gt;Someone who worked with Rails in 2010 can pick up a Rails application in 2025 and still recognize core patterns, conventions and commands. It all remained fundamentally similar.&lt;/p&gt;

&lt;h2&gt;Knowledge retention&lt;/h2&gt;

&lt;p&gt;The skills developers build working with Rails tend to stay relevant for years, which is rare in the fast–moving web development world.&lt;/p&gt;

&lt;h2&gt;Team flexibility&lt;/h2&gt;

&lt;p&gt;New team members who have Rails experience can become productive quickly without extensive onboarding.&lt;/p&gt;

&lt;h2&gt;Documentation stability&lt;/h2&gt;

&lt;p&gt;Solutions and patterns documented years ago often still apply, creating a rich knowledge base that remains useful.&lt;/p&gt;

&lt;h2&gt;Reduced &amp;quot;framework fatigue&amp;quot;&lt;/h2&gt;

&lt;p&gt;While Rails has evolved, it hasn’t required developers to 
completely relearn their workflow every quarter or two like JS ecosystem does.&lt;/p&gt;

&lt;p&gt;This stability creates enormous practical value. It means companies can maintain Rails applications for the long term without constantly rewriting them to keep up with framework changes. It also means the pool of developers who can work on a Rails project is broader, including those who may have been away from Rails for years but can quickly get back up to speed.&lt;/p&gt;

&lt;p&gt;From a domain modeling perspective, this stability means you can focus on evolving your business logic and domain models without constantly rebuilding the technical foundation underneath them.&lt;/p&gt;

&lt;p&gt;I smile to myself every time I see a JS app team implementing from scratch a framework feature that is simply available in Rails. Apparently, not everyone gets a chance to work with a mature web framework.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2024-06-13:/blog/how-to-add-index-to-big-table-of-your-rails-app/</id>
    <title type="html">How to add index to a big table of your Rails app</title>
    <published>2024-06-13T11:16:29Z</published>
    <updated>2024-06-13T11:16:29Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/how-to-add-index-to-big-table-of-your-rails-app/" type="text/html"/>
    <content type="html">&lt;h1&gt;How to add index to a big table of your Rails app&lt;/h1&gt;

&lt;p&gt;When your application is successful, some of the tables can grow pretty big — I’m looking at you &lt;em&gt;users&lt;/em&gt; table. If you’re curious enough, you periodically check how your database performs. If any slow query pops up in the metrics, there’s a great chance that some index is missing. &lt;/p&gt;

&lt;!-- more --&gt;

&lt;h2&gt;State of the DB engines&lt;/h2&gt;

&lt;p&gt;While most modern database engines can create indexes in an asynchronous, non–blocking manner, it’s good to get familiar with all the exceptions from this rule. I highly recommend reading the documentation of &lt;a href="https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY"&gt;PostgreSQL&lt;/a&gt;, &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html#online-ddl-index-operations"&gt;MySQL&lt;/a&gt;. I’m not sure about SQLite here as the documentation doesn’t clearly state that. However, my &lt;a href="https://chatgpt.com/share/248e939b-0fa4-49ad-a691-8535bab7dd08"&gt;quick chit–chat with LLM&lt;/a&gt; may give you some insights.&lt;/p&gt;

&lt;h2&gt;What’s the problem then?&lt;/h2&gt;

&lt;p&gt;As you already know &lt;code&gt;CREATE INDEX&lt;/code&gt; statement will be handled asynchronously by the database if appropriate algorithm is used. This means that no reads, writes and update will be blocked. &lt;/p&gt;

&lt;p&gt;Typically, for the Rails application, you’ll run a migration via Ruby process during deployment, using all the goodies from &lt;code&gt;ActiveRecord::Migration&lt;/code&gt; class and its surroundings. &lt;/p&gt;

&lt;p&gt;Let’s imagine the database schema like that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;version: &lt;/span&gt;&lt;span class="mi"&gt;2024_06_13_121701&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;force: :cascade&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="s2"&gt;"active"&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="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;Imagine you want to quickly find all those users which didn’t activate the account:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you have enough &lt;em&gt;users&lt;/em&gt;, speaking of dozens or hundreds of millions, doing full table scan could simply kill the database performance. Full table scan happens when database has no index to use and need to check every row whether it meets the criteria.&lt;/p&gt;

&lt;p&gt;I will stick with PostreSQL in the examples if not stated otherwise.&lt;/p&gt;

&lt;h2&gt;Obvious solution&lt;/h2&gt;

&lt;p&gt;Let’s add the index then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  trololo git:&lt;span class="o"&gt;(&lt;/span&gt;master&lt;span class="o"&gt;)&lt;/span&gt; bin/rails g migration AddIndexOnActiveUsers
      invoke  active_record
      create    db/migrate/20240613121751_add_index_on_active_users.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Implementation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddIndexOnActiveUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;disable_ddl_transaction!&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :concurrently&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# MySQL&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddIndexOnActiveUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :inplace&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;Let’s run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  trololo git:(master) bin/rails db:migrate
== 20240613121751 AddIndexOnActiveUsers: migrating ============================
-- add_index(:users, :active)
   -&amp;gt; 0.0013s
== 20240613121751 AddIndexOnActiveUsers: migrated (0.0013s) ===================
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ok, it was quite fast, right? That’s correct for your dev machine, not necessarily for the production setup. Now magnify this time &lt;code&gt;0.0013s&lt;/code&gt; by 6 or 7 orders of magnitude if your table is big enough:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜  trololo git:(master) bin/rails db:migrate
== 20240613121751 AddIndexOnActiveUsers: migrating ============================
-- add_index(:users, :active)
   -&amp;gt; 13000.4928s
== 20240613121751 AddIndexOnActiveUsers: migrated (0.0013s) ===================
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I’ll do the maths for you: &lt;em&gt;1300.4928s&lt;/em&gt; means &lt;em&gt;21 minutes 40.49 seconds&lt;/em&gt;. But it can be even longer — don’t ask me how I found about this.&lt;/p&gt;

&lt;p&gt;While the process of migration will end up eventually, the  migration blocking any other deployments to your application during this time may be unacceptable for various reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you cannot release any other change to production until migration completes&lt;/li&gt;
&lt;li&gt;something other goes wrong and you need to rapidly deploy a hotfix, but you can’t since the deployment is blocked by &lt;em&gt;long running migration™&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;process manager on a deployment machine may expect output within, e.g. 5 minutes. Long running migration will get killed in such scenario.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What to do then?&lt;/h2&gt;

&lt;p&gt;Simply skip the migration body for &lt;code&gt;RAILS_ENV=production&lt;/code&gt;:&lt;/p&gt;
&lt;div class="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;AddIndexOnActiveUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;disable_ddl_transaction!&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :concurrently&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;unless&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The migration will be executed on production environment, but will have no effect on your database. &lt;code&gt;schema.rb&lt;/code&gt; or &lt;code&gt;structure.sql&lt;/code&gt; (depending on what you use) will be aligned. Sufficient entry will also appear in &lt;code&gt;schema_migrations&lt;/code&gt; table. All the developers will have index added on their local databases, test environment will be aligned too.&lt;/p&gt;

&lt;p&gt;But &lt;em&gt;Hey, where’s my index on production?!&lt;/em&gt; you might ask. And that’s a pretty valid question. What you’ll need is a way to run a rails runner or rake task on production. Depending on the capabilities you have, you might choose to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it within &lt;code&gt;bin/rails console&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# PostgreSQL&lt;/span&gt;
&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :concurrently&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# MySQL&lt;/span&gt;
&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :inplace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &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;Do the same via &lt;code&gt;bin/rails runner&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PostgreSQL&lt;/span&gt;
bin/rails r &lt;span class="s2"&gt;"ActiveRecord::Migration.add_index :users, :active, algorithm: :concurrently, if_not_exists: true"&lt;/span&gt;

&lt;span class="c"&gt;#MySQL&lt;/span&gt;
bin/rails r &lt;span class="s2"&gt;"ActiveRecord::Migration.add_index :users, :active, algorithm: :inplace, if_not_exists: true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Last, but not least, implement a &lt;code&gt;Rake&lt;/code&gt; task. It has the advantage that it has to be committed to the repository so you don’t lose the history what’s happened:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# PostgreSQL&lt;/span&gt;
&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:indexes&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;add_index_on_active_users: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :concurrently&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &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;span class="c1"&gt;# MySQL&lt;/span&gt;
&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:indexes&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;add_index_on_active_users: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :inplace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &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;Execute it with &lt;code&gt;bin/rails indexes:add_index_on_active_users&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the last option it’s also easy to enhance it with logging to easily identify execution in &lt;em&gt;Grafana&lt;/em&gt;, &lt;em&gt;Datadog&lt;/em&gt; or any other tool you use for your logs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:indexes&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;add_index_on_active_users: :environment&lt;/span&gt; &lt;span class="k"&gt;do&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"task indexes:add_index_on_active_users started"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;algorithm: :concurrently&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if_not_exists: &lt;/span&gt;&lt;span class="kp"&gt;true&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"task indexes:add_index_on_active_users finished”)
  end
end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Tiny details&lt;/h3&gt;

&lt;p&gt;If you’re aware enough, you probably spotted &lt;code&gt;if_not_exists: true&lt;/code&gt; flag. We like idempotence and that’s the reason. If anyone runs this task again, nothing will happen. If you prefer to see &lt;code&gt;ActiveRecord::StatementInvalid&lt;/code&gt; instead, feel free to skip it.&lt;/p&gt;

&lt;p&gt;As mentioned in the preface, to use appropriate algorithm for index creation &lt;code&gt;algorithm: :concurrently&lt;/code&gt; for &lt;em&gt;PostgreSQL&lt;/em&gt; and &lt;code&gt;algorithm: inplace&lt;/code&gt; for &lt;em&gt;MySQL&lt;/em&gt; has to be specified.&lt;/p&gt;

&lt;p&gt;There’s another quirk for &lt;em&gt;PostgreSQL&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Another difference is that a regular &lt;code&gt;CREATE INDEX&lt;/code&gt; command can be performed within a transaction block, but &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; cannot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each Rails migration execution is wrapped within transaction. To disable this behavior, you need to use &lt;code&gt;disable_ddl_transaction!&lt;/code&gt; method within your migration. Otherwise, following error will pop up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails aborted!
StandardError: An error has occurred, this and all later migrations canceled: &lt;span class="o"&gt;(&lt;/span&gt;StandardError&lt;span class="o"&gt;)&lt;/span&gt;

PG::ActiveSqlTransaction: ERROR:  CREATE INDEX CONCURRENTLY cannot run inside a transaction block
/Users/fidel/code/fidel/trololo/db/migrate/20240613121751_add_index_on_active_users.rb:5:in &lt;span class="sb"&gt;`&lt;/span&gt;change&lt;span class="s1"&gt;'

Caused by:
ActiveRecord::StatementInvalid: PG::ActiveSqlTransaction: ERROR:  CREATE INDEX CONCURRENTLY cannot run inside a transaction block (ActiveRecord::StatementInvalid)
/Users/fidel/code/fidel/trololo/db/migrate/20240613121751_add_index_on_active_users.rb:5:in `change'&lt;/span&gt;

Caused by:
PG::ActiveSqlTransaction: ERROR:  CREATE INDEX CONCURRENTLY cannot run inside a transaction block &lt;span class="o"&gt;(&lt;/span&gt;PG::ActiveSqlTransaction&lt;span class="o"&gt;)&lt;/span&gt;
/Users/fidel/code/fidel/trololo/db/migrate/20240613121751_add_index_on_active_users.rb:5:in &lt;span class="sb"&gt;`&lt;/span&gt;change&lt;span class="s1"&gt;'
Tasks: TOP =&amp;gt; db:migrate
(See full trace by running task with --trace)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However, it’s not a problem for our custom script or &lt;code&gt;Rake&lt;/code&gt; task run on production environment.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2024-03-29:/blog/replace-aasm-with-rails-enum-today/</id>
    <title type="html">Replace aasm with Rails Enum today</title>
    <published>2024-03-29T22:36:29Z</published>
    <updated>2024-03-29T22:36:29Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/replace-aasm-with-rails-enum-today/" type="text/html"/>
    <content type="html">&lt;h1&gt;Replace aasm with Rails Enum today&lt;/h1&gt;

&lt;p&gt;There’s a great chance that your Rails app contains one of the gems providing so called state machine implementation. There’s even a greater chance that it will be &lt;a href="https://github.com/aasm/aasm"&gt;aasm&lt;/a&gt; formerly known as &lt;code&gt;acts_as_state_machine&lt;/code&gt;. Btw. Who remembers &lt;a href="https://github.com/mariozig/acts_as_hasselhoff"&gt;acts_as_hasselhoff&lt;/a&gt;? — ok, boomer. The &lt;em&gt;aasm&lt;/em&gt; does quite a lot when included into your &lt;code&gt;ActiveRecord&lt;/code&gt; model — the question is do you really need all those things?&lt;/p&gt;

&lt;!-- more --&gt;

&lt;h2&gt;My problem with aasm&lt;/h2&gt;

&lt;p&gt;I was struck by reckless use of this gem so many times that first thing I do after joining a new project is running &lt;code&gt;cat Gemfile | grep aasm&lt;/code&gt; and here comes the meme which I made ~1.5 years ago:&lt;/p&gt;

&lt;p&gt;&lt;img src="" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;My main concern with use of this gem is that you probably don’t need all the features it offers. More features means more temptation to use them. Greater use across the codebase means more coupling to external library which can be incompatible with upcoming Rails versions, blocking you from upgrade or making it pretty costly. You have to read yet another changelog to check if there aren’t any breaking changes or some subtle behavior change running your serious business application into problems.&lt;/p&gt;

&lt;p&gt;Next thing is that it promotes patterns like callbacks which I personally see as a huge problem within complex rails applications. I prefer explicit code, callbacks aren’t such. &lt;em&gt;Ackchyually callbacks are a part of the framework&lt;/em&gt;, I know, but let’s leave this discussion for another place in time.&lt;/p&gt;

&lt;p&gt;It’s also struggle for all the IDEs, Language Server Providers to find definitions of methods defined by &lt;em&gt;aasm’s&lt;/em&gt; DSL and you are forced to use &lt;code&gt;grep&lt;/code&gt;, read the code carefully and decide whether those &lt;code&gt;active!&lt;/code&gt; method comes from or where my &lt;code&gt;pending&lt;/code&gt; scope is defined.&lt;/p&gt;

&lt;p&gt;I’ve recently learned that it also autogenerates constants for each state &lt;em&gt;so I don’t have to&lt;/em&gt;. I’ve recently spotted &lt;code&gt;Transaction::STATUS_FAILED&lt;/code&gt; somewhere, it took quite some time to figure out the origin of this constant which wasn’t explicitly defined within the &lt;code&gt;Transaction&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Those &lt;em&gt;free lunches&lt;/em&gt; can be tasty, but eventually you will be forced to pay for them.&lt;/p&gt;

&lt;p&gt;Last, but not least, having attribute like &lt;em&gt;state&lt;/em&gt; or &lt;em&gt;status&lt;/em&gt; often suggest poor design in your codebase, but that’s a totally different story.&lt;/p&gt;

&lt;h2&gt;Starting point&lt;/h2&gt;

&lt;p&gt;Nine years old legacy Rails app backing very successful business. Let’s have a look at some of the details:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;-&amp;gt; ruby &lt;span class="nt"&gt;-v&lt;/span&gt;
ruby 3.3.0 &lt;span class="o"&gt;(&lt;/span&gt;2023-12-25 revision 5124f9ac75&lt;span class="o"&gt;)&lt;/span&gt;

-&amp;gt; bin/rails r &lt;span class="s2"&gt;"p Rails.version"&lt;/span&gt;
&lt;span class="s2"&gt;"7.1.3.2"&lt;/span&gt;

-&amp;gt; bundle list | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
319

-&amp;gt; rg include&lt;span class="se"&gt;\ &lt;/span&gt;AASM &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
33
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Few uses of &lt;code&gt;model.aasm.states.map(&amp;amp;:name)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two uses of &lt;code&gt;SwitchRequest.aasm.states_for_select&lt;/code&gt; to provide options for some &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;h2&gt;Rails, the white knight&lt;/h2&gt;

&lt;p&gt;As a Rails developer, you’re probably familiar with &lt;em&gt;enum&lt;/em&gt; which was &lt;a href="https://api.rubyonrails.org/v6.0/classes/ActiveRecord/Enum.html"&gt;introduced in Rails 6.0&lt;/a&gt; and allowed to declare an attribute where the values map to integers in the database. It evolved a bit with next framework versions, but Rails 7.1 finally brought all the features required to replace all of the &lt;em&gt;aasm&lt;/em&gt; uses in the codebase I’m currently working on.&lt;/p&gt;

&lt;p&gt;Let’s make use of this simple class as an example for our further work:&lt;/p&gt;
&lt;div class="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;Transaction&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="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;AASM&lt;/span&gt;

  &lt;span class="no"&gt;PROCESSING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:processing&lt;/span&gt;
  &lt;span class="no"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:failed&lt;/span&gt;
  &lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:successful&lt;/span&gt;
  &lt;span class="no"&gt;CANCELED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:canceled&lt;/span&gt;

  &lt;span class="n"&gt;aasm&lt;/span&gt; &lt;span class="ss"&gt;column: :status&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;initial: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="no"&gt;FAILED&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="no"&gt;CANCELED&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;h3&gt;What’s required to get exact the same behavior&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scopes&lt;/strong&gt; like &lt;code&gt;Transaction.successful&lt;/code&gt; to query all the transactions having status &lt;code&gt;successful&lt;/code&gt; — we can have those for free from &lt;code&gt;ActiveRecord::Enum&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance methods&lt;/strong&gt; to:

&lt;ul&gt;
&lt;li&gt;check whether our object’s status is &lt;code&gt;successful?&lt;/code&gt; — &lt;em&gt;enum&lt;/em&gt; got you covered&lt;/li&gt;
&lt;li&gt;change the status to &lt;code&gt;successful&lt;/code&gt; and run &lt;code&gt;save!&lt;/code&gt; as we got used to it — same here, &lt;em&gt;enum&lt;/em&gt; will do it’s job here if you call &lt;code&gt;transaction.successful!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Set desired &lt;strong&gt;initial state&lt;/strong&gt; for new objects — this can be done by providing &lt;code&gt;default&lt;/code&gt; keyword argument to &lt;code&gt;enum&lt;/code&gt; method, like &lt;code&gt;default: PROCESSING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Values need to be stored as strings, not as integers in the db — it’s possible since &lt;a href="https://api.rubyonrails.org/v7.0/files/activerecord/lib/active_record/enum_rb.html"&gt;Rails 7.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Hopefully there were no transitions or guards in state machines in our application — yet another reason to not employ &lt;em&gt;aasm&lt;/em&gt; — but I can image implementing it with &lt;code&gt;ActiveRecord::Dirty&lt;/code&gt; quite easy. Or even better, implement this behavior as a higher level abstracion and keep you model dummy in this matter.&lt;/li&gt;
&lt;li&gt;No callbacks either, yay!&lt;/li&gt;
&lt;li&gt;Constants containing all the possible states were already in place, so those defined by &lt;em&gt;aasm&lt;/em&gt;, like &lt;code&gt;STATUS_PROCESSING, STATUS_SUCCESSFUL&lt;/code&gt; and so on were something nobody asked for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Validation of the provided value&lt;/h4&gt;

&lt;p&gt;There’s slight difference in default &lt;em&gt;enum&lt;/em&gt; behavior. If the provided a value doesn’t match specified values, you will be struck with &lt;code&gt;ArgumentError&lt;/code&gt; when trying to assign one.&lt;/p&gt;

&lt;p&gt;As mentioned earlier, in Rails 7.1 there’s a possibility to provide &lt;code&gt;validate: true&lt;/code&gt; keyword argument. &lt;em&gt;Enum&lt;/em&gt; will behave exactly the same as &lt;em&gt;aasm&lt;/em&gt; in this manner which checks the validity of provided value before save resulting in &lt;code&gt;ActiveRecord::RecordInvalid&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;Show me the code&lt;/h3&gt;
&lt;div class="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;Transaction&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="no"&gt;PROCESSING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:processing&lt;/span&gt;
    &lt;span class="no"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:failed&lt;/span&gt;
    &lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:successful&lt;/span&gt;
    &lt;span class="no"&gt;CANCELED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:canceled&lt;/span&gt;

    &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;processing: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;failed: &lt;/span&gt;&lt;span class="no"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;successful: &lt;/span&gt;&lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;canceled: &lt;/span&gt;&lt;span class="no"&gt;CANCELED&lt;/span&gt;
         &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;transform_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
         &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&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="ss"&gt;validate: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Quit clean and understandable, isn’t it?&lt;/p&gt;

&lt;h2&gt;The values quirk&lt;/h2&gt;

&lt;p&gt;There’s one quirk, you’ve probably already noticed: &lt;code&gt;transform_values(&amp;amp;:to_s)&lt;/code&gt;. I was quite confused what’s going on when I’ve provided symbols as values initially. Let’s see how the code looked like before:&lt;/p&gt;
&lt;div class="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;Transaction&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="no"&gt;PROCESSING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:processing&lt;/span&gt;
    &lt;span class="no"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:failed&lt;/span&gt;
    &lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:successful&lt;/span&gt;
    &lt;span class="no"&gt;CANCELED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:canceled&lt;/span&gt;

    &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;processing: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;failed: &lt;/span&gt;&lt;span class="no"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;successful: &lt;/span&gt;&lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;canceled: &lt;/span&gt;&lt;span class="no"&gt;CANCELED&lt;/span&gt;
         &lt;span class="p"&gt;},&lt;/span&gt;
         &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;validate: &lt;/span&gt;&lt;span class="kp"&gt;true&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 documentation hasn’t clearly stated that values should be strings. The example used &lt;code&gt;String&lt;/code&gt; values, but there was no clear expectation about that. However, when saving the object, &lt;code&gt;status&lt;/code&gt; became &lt;code&gt;nil&lt;/code&gt; at some point, despite assigning &lt;code&gt;”successful”&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;I wrote a dummy app and test for this specific case aside from the main application I was working on. To be 100% sure that there’s no other factor influences this behavior:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'bundler/inline'&lt;/span&gt;

&lt;span class="n"&gt;gemfile&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://rubygems.org'&lt;/span&gt;

  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'activerecord'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'sqlite3'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'minitest'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'active_record'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'sqlite3'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;

&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;db_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enum_test.sqlite3'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="no"&gt;SQLite3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Database&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;db_name&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;establish_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;adapter: &lt;/span&gt;&lt;span class="s1"&gt;'sqlite3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;database: &lt;/span&gt;&lt;span class="n"&gt;db_name&lt;/span&gt;&lt;span class="p"&gt;)&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;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:transactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:status&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;Transaction&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="no"&gt;PROCESSING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:processing&lt;/span&gt;
    &lt;span class="no"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:failed&lt;/span&gt;
    &lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:successful&lt;/span&gt;
    &lt;span class="no"&gt;CANCELED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:canceled&lt;/span&gt;

    &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;processing: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;failed: &lt;/span&gt;&lt;span class="no"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;successful: &lt;/span&gt;&lt;span class="no"&gt;SUCCESSFUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;canceled: &lt;/span&gt;&lt;span class="no"&gt;CANCELED&lt;/span&gt;
         &lt;span class="p"&gt;},&lt;/span&gt;
         &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="no"&gt;PROCESSING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="ss"&gt;validate: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestTransaction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_enum_behavior&lt;/span&gt;
      &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Transaction&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;status: :successful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="s1"&gt;'successful'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;

      &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;
      &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="s1"&gt;'successful'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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;ensure&lt;/span&gt;
  &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_name&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;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;ruby enum.rb
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:transactions, &lt;span class="o"&gt;{&lt;/span&gt;:force&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0076s
Run options: &lt;span class="nt"&gt;--seed&lt;/span&gt; 3038

&lt;span class="c"&gt;# Running:&lt;/span&gt;

F

Finished &lt;span class="k"&gt;in &lt;/span&gt;0.013456s, 74.3163 runs/s, 148.6326 assertions/s.

  1&lt;span class="o"&gt;)&lt;/span&gt; Failure:
TestTransaction#test_enum &lt;span class="o"&gt;[&lt;/span&gt;enum.rb:44]:
Expected: &lt;span class="s2"&gt;"successful"&lt;/span&gt;
  Actual: nil

1 runs, 2 assertions, 1 failures, 0 errors, 0 skips
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ok, so the value is correctly set, but it disappears when &lt;code&gt;save!&lt;/code&gt; is called. After quick session with debugger I was able to figure out that undesired change from &lt;code&gt;&amp;quot;successful&amp;quot;&lt;/code&gt; to &lt;code&gt;nil&lt;/code&gt; happens inside &lt;a href="https://github.com/rails/rails/blob/6f0d1ad14b92b9f5906e44740fce8b4f1c7075dc/activerecord/lib/active_record/enum.rb#L190-L192"&gt;Enum::EnumType#deserialize&lt;/a&gt; method. Generic &lt;code&gt;ActiveModel::Type::Value&lt;/code&gt; class which is a parent for the &lt;code&gt;ActiveRecord::Enum::EnumType&lt;/code&gt; describes the purpose of &lt;code&gt;deserialize&lt;/code&gt; method like that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from &lt;code&gt;ActiveRecord::AttributeMethods::Read#read_attribute&lt;/code&gt;. The default implementation just calls &lt;code&gt;Value#cast&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s have a  look at our specific scenario:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# value = "successful"&lt;/span&gt;
&lt;span class="c1"&gt;# mapping = ActiveSupport::HashWithIndifferentAccess.new({&lt;/span&gt;
&lt;span class="c1"&gt;#  "processing" =&amp;gt; :processing,&lt;/span&gt;
&lt;span class="c1"&gt;#  "failed" =&amp;gt; :failed,&lt;/span&gt;
&lt;span class="c1"&gt;#  "successful" =&amp;gt; :successful,&lt;/span&gt;
&lt;span class="c1"&gt;#  "canceled" =&amp;gt; :canceled,&lt;/span&gt;
&lt;span class="c1"&gt;# })&lt;/span&gt;
&lt;span class="c1"&gt;# subtype = ActiveModel::Type::String instance&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;What exactly happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;subtype.deserialize(value)&lt;/code&gt; returns &lt;code&gt;”successful”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapping&lt;/code&gt; is asked to return a key for &lt;code&gt;”successful”&lt;/code&gt; value, but there is no such in the hash, there’s a &lt;code&gt;Symbol&lt;/code&gt; &lt;code&gt;:succesful&lt;/code&gt; — yikes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deserialize(&amp;quot;successful&amp;quot;)&lt;/code&gt; returns &lt;code&gt;nil&lt;/code&gt; instead of &lt;code&gt;&amp;quot;successful&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Probably a single line in the docs like: &lt;em&gt;Don’t put symbols as values when defining the enum&lt;/em&gt; would do the job and save my time and potentially many other confused developers. What’s even more puzzling is the fact that you can provide default value as a &lt;code&gt;Symbol&lt;/code&gt;, you can assign a value which is a &lt;code&gt;Symbol&lt;/code&gt; and it will be stored as a &lt;code&gt;String&lt;/code&gt;, which is understandable, but the definition has to contain string values — hence the &lt;code&gt;.transform_values(&amp;amp;:to_s)&lt;/code&gt; trick.&lt;/p&gt;

&lt;p&gt;However, &lt;em&gt;why use symbols to define possible state?&lt;/em&gt;, you may ask. Because it’s a requirement of &lt;em&gt;aasm&lt;/em&gt;. State has to be &lt;code&gt;Symbol&lt;/code&gt; or object responding to &lt;code&gt;#name&lt;/code&gt; method. If you provide &lt;code&gt;String&lt;/code&gt;, you’ll see nice &lt;code&gt;NoMethodError&lt;/code&gt; because it doesn’t respond to &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You know what’s even funnier? Database returns &lt;code&gt;String&lt;/code&gt; when asked for &lt;code&gt;Transactions#status&lt;/code&gt;. Serializing it to &lt;em&gt;JSON&lt;/em&gt; will also turn it into &lt;code&gt;String&lt;/code&gt; as &lt;em&gt;JSON&lt;/em&gt; doesn’t implement symbols. I could imagine more scenarios when constant casting from &lt;code&gt;Symbol&lt;/code&gt; to &lt;code&gt;String&lt;/code&gt; back and forth happen without any particular reason. But that’s exactly how 3rd party gem (&lt;em&gt;AASM&lt;/em&gt;) drives your architectural decisions.&lt;/p&gt;

&lt;h2&gt;We can do even better&lt;/h2&gt;

&lt;p&gt;If you aren’t using scopes, you can simply disable them with &lt;code&gt;scopes: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same goes for instance methods like &lt;code&gt;succesful!&lt;/code&gt; or &lt;code&gt;failed?&lt;/code&gt;, those can be disabled with &lt;code&gt;instance_methods: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most of the models that was rewritten required both, but whenever it was possible I disabled both of the features.&lt;/p&gt;

&lt;p&gt;Occurrences of &lt;code&gt;Model.aasm.states.map(&amp;amp;:name)&lt;/code&gt; are replaceable by &lt;code&gt;Model.statuses.values&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Model.aasm.states_for_select&lt;/code&gt; helper can be replaced with &lt;code&gt;Model.statuses.values.map { |name, value| [I18n.l(name), value] }&lt;/code&gt;. Extract this to one of your helpers.
Maybe you don’t need to call &lt;code&gt;I18n&lt;/code&gt; at all and simple &lt;code&gt;humanize&lt;/code&gt; will be enough.&lt;/p&gt;

&lt;p&gt;Now it’s your turn.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:fiedler.pro,2023-08-31:/blog/the-most-underused-pattern-in-ruby/</id>
    <title type="html">The most underused pattern in Ruby</title>
    <published>2023-08-31T17:04:08Z</published>
    <updated>2023-08-31T17:04:08Z</updated>
    <link rel="alternate" href="https://fiedler.pro/blog/the-most-underused-pattern-in-ruby/" type="text/html"/>
    <content type="html">&lt;h1&gt;The most underused pattern in Ruby&lt;/h1&gt;

&lt;p&gt;Recently one of the RailsEventStore users posted an &lt;a href="https://github.com/RailsEventStore/rails_event_store/issues/1650"&gt;issue&lt;/a&gt; that one wanted to use RES on a Postgres database with &lt;a href="https://postgis.net"&gt;PostGIS&lt;/a&gt; extension. Migration generator used to setup tables for events and streams was failing with &lt;code&gt;UnsupportedAdapter&lt;/code&gt; error.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;In RailsEventStore we supported to date &lt;em&gt;PostgreSQL&lt;/em&gt;, &lt;em&gt;MySQL2&lt;/em&gt; and &lt;em&gt;SQLite&lt;/em&gt; adapters representing given database engines. But if you want to make use of mentioned &lt;em&gt;PostGIS&lt;/em&gt; extension, you need install additional &lt;code&gt;activerecord-postgis-adapter&lt;/code&gt; and set &lt;code&gt;adapter: postgis&lt;/code&gt; in &lt;code&gt;database.yml&lt;/code&gt;. Our code relied on value returned by:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I thought — &lt;em&gt;Ok, that&amp;#39;s an easy fix&lt;/em&gt;, &lt;em&gt;PostGIS&lt;/em&gt; is just an extension, we need to treat it like Postgres internally when generating migration. Same data types should be allowed. &lt;/p&gt;

&lt;h2&gt;Easy fix, requiring a lot of changes&lt;/h2&gt;

&lt;p&gt;This easy fix required me to change 8 files (4 with implementation and 4 with tests). &lt;em&gt;Something is not ok here&lt;/em&gt; — I thought. So let&amp;#39;s look through each of them:&lt;/p&gt;

&lt;p&gt;I had to add &lt;code&gt;postgis&lt;/code&gt; to the list of &lt;code&gt;SUPPORTED_ADAPTERS&lt;/code&gt; in &lt;code&gt;VerifyAdapter&lt;/code&gt; class&lt;/p&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&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;StandardError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerifyAdapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="no"&gt;SUPPORTED_ADAPTERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[mysql2 postgresql sqlite]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="no"&gt;SUPPORTED_ADAPTERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[mysql2 postgresql postgis sqlite]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Unsupported adapter"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;supported?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&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="n"&gt;private_constant&lt;/span&gt; &lt;span class="ss"&gt;:SUPPORTED_ADAPTERS&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;supported?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="no"&gt;SUPPORTED_ADAPTERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I had to extend case statement in &lt;code&gt;ForeignKeyOnEventIdMigrationGenerator#each_migration&lt;/code&gt; method&lt;/p&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ForeignKeyOnEventIdMigrationGenerator&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="no"&gt;VerifyAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;each_migration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&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="kp"&gt;private&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;each_migration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="s1"&gt;'add_foreign_key_on_event_id_to_event_store_events_in_streams'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s1"&gt;'validate_add_foreign_key_on_event_id_to_event_store_events_in_streams'&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'add_foreign_key_on_event_id_to_event_store_events_in_streams'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;absolute_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&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;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&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;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;migration_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;result_with_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;migration_version: &lt;/span&gt;&lt;span class="n"&gt;migration_version&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;migration_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_root&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;ERB&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;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_template.erb"&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;template_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;absolute_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./templates/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="no"&gt;TemplateDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&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;migration_version&lt;/span&gt;
          &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_version&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;timestamp&lt;/span&gt;
          &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%Y%m%d%H%M%S"&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;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_code&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;build_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="p"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;migration_path&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="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&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;migration_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.rb"&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Same goes for Rails version of migration generator&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rails/generators"&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;LoadError&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;defined?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Generators&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RubyEventStore&lt;/span&gt;
      &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
        &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RailsForeignKeyOnEventIdMigrationGenerator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Generators&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
          &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Thor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;

          &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="s2"&gt;"rails_event_store_active_record:migration_for_foreign_key_on_event_id"&lt;/span&gt;

          &lt;span class="n"&gt;source_root&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;expand_path&lt;/span&gt;&lt;span class="p"&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;join&lt;/span&gt;&lt;span class="p"&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;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"../generators/templates"&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;initialize&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;super&lt;/span&gt;

            &lt;span class="no"&gt;VerifyAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&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="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;

          &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_migration&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;           &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;           &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
              &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
              &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_validate_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
              &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
            &lt;span class="k"&gt;end&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;

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

          &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;
            &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;

          &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migration_version&lt;/span&gt;
            &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_version&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;timestamp&lt;/span&gt;
            &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%Y%m%d%H%M%S"&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;template_directory&lt;/span&gt;
            &lt;span class="no"&gt;TemplateDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&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;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;What is important, both of the migrators used &lt;code&gt;VerifyAdapter&lt;/code&gt; class (and two other migrators too).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TemplateDirectory&lt;/code&gt; class also suffered from primitive obsession and it was used by all of the migrators too.&lt;/p&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TemplateDirectory&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
            &lt;span class="s2"&gt;"postgres/"&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"mysql2"&lt;/span&gt;
            &lt;span class="s2"&gt;"mysql/"&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;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;There was also one more place — &lt;code&gt;VerifyDataTypeForAdapter&lt;/code&gt; which was composed of &lt;code&gt;VerifyAdapter&lt;/code&gt;, adding verification of data types available to given database engine.&lt;/p&gt;

&lt;p&gt;Here we go again, another checks of string values, but in a more specific context:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RubyEventStore&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
    &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&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;StandardError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerifyDataTypeForAdapter&lt;/span&gt;
      &lt;span class="no"&gt;SUPPORTED_POSTGRES_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json jsonb]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
      &lt;span class="no"&gt;SUPPORTED_MYSQL_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
      &lt;span class="no"&gt;SUPPORTED_SQLITE_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;VerifyAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"MySQL2 doesn't support &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_mysql2?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&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="no"&gt;SUPPORTED_MYSQL_DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sqlite doesn't support &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_sqlite?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;supported_by_sqlite?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL doesn't support &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;supported_by_postgres?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&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="n"&gt;private_constant&lt;/span&gt; &lt;span class="ss"&gt;:SUPPORTED_POSTGRES_DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:SUPPORTED_MYSQL_DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:SUPPORTED_SQLITE_DATA_TYPES&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_sqlite?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sqlite"&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;is_mysql2?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"mysql2"&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;supported_by_sqlite?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="no"&gt;SUPPORTED_SQLITE_DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&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;supported_by_postgres?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;SUPPORTED_POSTGRES_DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;I&amp;#39;ve noticed the pattern&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;at start we need to check whether given adapter is allowed (PostgreSQL, MySQL, SQLite)&lt;/li&gt;
&lt;li&gt;we need to verify certain data types for given adapters to be aligned with database engines&lt;/li&gt;
&lt;li&gt;then we have to made a decision to generate specific migration for given data type&lt;/li&gt;
&lt;li&gt;migration template directory depends on adapter type&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Let&amp;#39;s do it&lt;/h2&gt;

&lt;p&gt;Having all this within a dedicated &lt;a href="https://blog.arkency.com/tags/value-object/"&gt;Value Object&lt;/a&gt; would allow reducing number of decision trees in the code, checking the same primitives on and on.&lt;/p&gt;

&lt;p&gt;Something like: &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bazinga"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"bazinga"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&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;UnsupportedAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sqlite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"jsonb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SQLite&lt;/span&gt; &lt;span class="n"&gt;doesn&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="s2"&gt;"jsonb"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="no"&gt;Supported&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="ss"&gt;are: &lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&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;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After few iterations we ended up with the implementation below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RubyEventStore&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
    &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&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;StandardError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&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;StandardError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseAdapter&lt;/span&gt;
      &lt;span class="no"&gt;NONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&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;freeze&lt;/span&gt;

      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
        &lt;span class="no"&gt;SUPPORTED_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json jsonb]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter_name&lt;/span&gt;
          &lt;span class="s2"&gt;"postgresql"&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;template_directory&lt;/span&gt;
          &lt;span class="s2"&gt;"postgres/"&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;MySQL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
        &lt;span class="no"&gt;SUPPORTED_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter_name&lt;/span&gt;
          &lt;span class="s2"&gt;"mysql2"&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;template_directory&lt;/span&gt;
          &lt;span class="s2"&gt;"mysql/"&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;SQLite&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
        &lt;span class="no"&gt;SUPPORTED_DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter_name&lt;/span&gt;
          &lt;span class="s2"&gt;"sqlite"&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;adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;instance_of?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;validate_data_type!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="vi"&gt;@data_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

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

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;supported_data_types&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="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUPPORTED_DATA_TYPES&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;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&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;alias&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;eql?&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;
        &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&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;template_directory&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NoMethodError&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nb"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
          &lt;span class="no"&gt;PostgreSQL&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;data_type&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;"mysql2"&lt;/span&gt;
          &lt;span class="no"&gt;MySQL&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;data_type&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;"sqlite"&lt;/span&gt;
          &lt;span class="no"&gt;SQLite&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;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Unsupported adapter: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&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="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_data_type!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;supported_data_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; doesn't support &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Supported types are: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;supported_data_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;class_name&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"::"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;DatabaseAdadpter&lt;/code&gt; acts like a parent class to all the specific adapters.
Specific adapters contain lists of &lt;code&gt;supported_data_types&lt;/code&gt; to access those by client classes and render informative error messages if selected data is not supported by given database engine.
They can also tell how the &lt;code&gt;template_directory&lt;/code&gt; is named  for given adapter.&lt;/p&gt;

&lt;p&gt;We have a single entry with &lt;code&gt;DatabaseAdapter.from_string&lt;/code&gt; which accepts &lt;code&gt;adapter_name&lt;/code&gt; and optionally &lt;code&gt;data_type&lt;/code&gt; which are both validated when creating an instance of specific adapter.&lt;/p&gt;

&lt;h2&gt;What&amp;#39;s the outcome?&lt;/h2&gt;

&lt;p&gt;Three utility classes could be removed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VerifyAdapter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VerifyDataTypeForAdapter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TemplateDirectory&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four classes and two rake tasks were simplified since the Value Object carriers all the necessary information for them to proceed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ForeignKeyOnEventIdMigrationGenerator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ForeignKeyOnEventIdMigrationGenerator&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="no"&gt;VerifyAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="n"&gt;database_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;each_migration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_name&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;each_migration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQL&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="s1"&gt;'add_foreign_key_on_event_id_to_event_store_events_in_streams'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s1"&gt;'validate_add_foreign_key_on_event_id_to_event_store_events_in_streams'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RailsForeignKeyOnEventIdMigrationGenerator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&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;super&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="no"&gt;VerifyAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="vi"&gt;@database_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&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="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_migration&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="vi"&gt;@database_adapter&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQL&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_validate_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;           &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter_name&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template_directory&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="no"&gt;TemplateDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MigrationGenerator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MigrationGenerator&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="no"&gt;DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json jsonb]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; 
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid value for data type. Supported for options are: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="no"&gt;VerifyDataTypeForAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; 
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="n"&gt;migration_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="n"&gt;migration_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migration_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;path&lt;/span&gt;


&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="n"&gt;migration_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"create_event_store_events"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;result_with_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;migration_version: &lt;/span&gt;&lt;span class="n"&gt;migration_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data_type: &lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migration_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="n"&gt;migration_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"create_event_store_events"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;result_with_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;migration_version: &lt;/span&gt;&lt;span class="n"&gt;migration_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data_type: &lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_type&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;template_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="n"&gt;absolute_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./templates/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; 
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="no"&gt;TemplateDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="n"&gt;absolute_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./templates/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RailsMigrationGenerator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="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;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Thor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt;     &lt;span class="no"&gt;DATA_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[binary json jsonb]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;
      &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="s2"&gt;"rails_event_store_active_record:migration"&lt;/span&gt;

      &lt;span class="n"&gt;source_root&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;expand_path&lt;/span&gt;&lt;span class="p"&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;join&lt;/span&gt;&lt;span class="p"&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;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"../generators/templates"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


        &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"binary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;desc:
&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="s2"&gt;"Configure the data type for `data` and `meta data` fields in Postgres migration (options: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="s2"&gt;"Configure the data type for `data` and `meta data` fields in migration (options: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostgreSQL&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;supported_data_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;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="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid value for --data-type option. Supported for options are: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;DATA_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="no"&gt;VerifyDataTypeForAdapter&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;     &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&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="o"&gt;+&lt;/span&gt;       &lt;span class="vi"&gt;@database_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;     &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&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="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;     &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;InvalidDataTypeForAdapter&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;             &lt;span class="s2"&gt;"Invalid value for --data-type option. Supported for options are: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;supported_data_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;def&lt;/span&gt; &lt;span class="nf"&gt;create_migration&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;create_event_store_events_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_create_event_store_events.rb"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@database_adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;create_event_store_events_template.erb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="s2"&gt;"db/migrate/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_create_event_store_events.rb"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;span class="o"&gt;-&lt;/span&gt;     &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template_directory&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="no"&gt;TemplateDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&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;data_type&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt;     &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;     &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapter_name&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;       &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;db:migrations:copy&lt;/code&gt; and &lt;code&gt;db:migrations:add_foreign_key_on_event_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"db:migrations:copy"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;data_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DATA_TYPE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Specify data type (binary, json, jsonb): rake db:migrations:copy DATA_TYPE=json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;establish_connection&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="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;   &lt;span class="n"&gt;database_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;   &lt;span class="n"&gt;database_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;     &lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&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;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;::&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;     &lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&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;MigrationGenerator&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="n"&gt;database_adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;       &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MIGRATION_PATH"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"db/migrate"&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="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&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;MigrationGenerator&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_adapter&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="s2"&gt;"MIGRATION_PATH"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"db/migrate"&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;"Migration file created &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;path&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="err"&gt;@@&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="err"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Generate migration for adding foreign key on event_store_events_in_streams
  task "&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="ss"&gt;:migrations:add_foreign_key_on_event_id&lt;/span&gt;&lt;span class="s2"&gt;" do
    ::ActiveRecord::Base.establish_connection(ENV["&lt;/span&gt;&lt;span class="no"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"])

-   path = RubyEventStore::ActiveRecord::ForeignKeyOnEventIdMigrationGenerator.new.call(ENV["&lt;/span&gt;&lt;span class="no"&gt;MIGRATION_PATH&lt;/span&gt;&lt;span class="s2"&gt;"] || "&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;migrate&lt;/span&gt;&lt;span class="s2"&gt;")
+   path =
+     RubyEventStore::ActiveRecord::ForeignKeyOnEventIdMigrationGenerator.new.call(ENV["&lt;/span&gt;&lt;span class="no"&gt;MIGRATION_PATH&lt;/span&gt;&lt;span class="s2"&gt;"] || "&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;migrate&lt;/span&gt;&lt;span class="s2"&gt;")

    puts "&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="c1"&gt;#{path}"&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 could reduce branching and remove numerous private methods in those.&lt;/p&gt;

&lt;p&gt;The tests of above classes simplified a lot and are now focused on core responsibilities of the classes rather than checking which data types are compatible with given adapter.&lt;/p&gt;

&lt;h2&gt;New adapter, not a big deal&lt;/h2&gt;

&lt;p&gt;Soon, there will be new MySQL adapter in Rails 7.1 called &lt;a href="https://github.blog/2022-08-25-introducing-trilogy-a-new-database-adapter-for-ruby-on-rails/"&gt;Trilogy&lt;/a&gt;. It would be cool to cover this case already.&lt;/p&gt;

&lt;p&gt;The only thing which we had to do in this case, was to change one line of code and add single line of test — since we already owned a good abstraction:&lt;/p&gt;
&lt;div class="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;RubyEventStore&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveRecord&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseAdapter&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NoMethodError&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nb"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"postgis"&lt;/span&gt;
            &lt;span class="no"&gt;PostgreSQL&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;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"mysql2"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"mysql2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"trilogy"&lt;/span&gt;
            &lt;span class="no"&gt;MySQL&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;data_type&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;"sqlite"&lt;/span&gt;
            &lt;span class="no"&gt;SQLite&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;data_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;UnsupportedAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Unsupported adapter: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;adapter_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&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="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;span class="o"&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;DatabaseAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Trilogy"&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;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DatabaseAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MySQL&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Trilogy&lt;/em&gt; is an adapter for &lt;em&gt;MySQL&lt;/em&gt;, there&amp;#39;s no difference from our perspective, we want to treat it as such.&lt;/p&gt;

&lt;h2&gt;Summary&lt;/h2&gt;

&lt;p&gt;If you&amp;#39;re curious on the full process, here&amp;#39;s the &lt;a href="https://github.com/RailsEventStore/rails_event_store/pull/1671/files"&gt;PR&lt;/a&gt; with the introduction of &lt;code&gt;DatabaseAdapter&lt;/code&gt; value object. The code is 100% covered with mutation testing thanks to &lt;a href="https://github.com/mbj/mutant"&gt;mutant&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I believe that Value Object is a totally underused pattern in Ruby ecosystem. That&amp;#39;s why I wanted to provide &lt;a href="https://blog.arkency.com/which-one-to-use-eql-vs-equals-vs-double-equal-mutant-driven-developpment-for-country-value-object/"&gt;yet another example&lt;/a&gt; which differs from typical &lt;code&gt;Money&lt;/code&gt; one you usually see.&lt;/p&gt;

&lt;p&gt;It&amp;#39;s a great tool to reduce complexity of your code by removing &lt;a href="https://academy.arkency.com/anti-ifs-main"&gt;unnecessary or repeatable branching&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
</feed>

