<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://blog.arkency.com/">
  <id>https://blog.arkency.com/</id>
  <title>Hi, we're Arkency</title>
  <updated>2026-02-20T11:20:24Z</updated>
  <link rel="alternate" href="https://blog.arkency.com/" type="text/html"/>
  <link rel="self" href="https://blog.arkency.com/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Arkency</name>
    <uri>https://arkency.com</uri>
  </author>
  <entry>
    <id>tag:blog.arkency.com,2026-02-20:/getting-nondeterministic-agent-into-deterministic-guardrails/</id>
    <title type="html">Getting nondeterministic agent into deterministic guardrails</title>
    <published>2026-02-20T11:20:24Z</published>
    <updated>2026-02-20T11:20:24Z</updated>
    <link rel="alternate" href="https://blog.arkency.com/getting-nondeterministic-agent-into-deterministic-guardrails/" type="text/html"/>
    <content type="html">&lt;h1 id="getting_nondeterministic_agent_into_deterministic_guardrails"&gt;Getting nondeterministic agent into deterministic guardrails&lt;/h1&gt;
&lt;p&gt;AI agents don&amp;rsquo;t reliably follow your instructions. Here&amp;rsquo;s how I made it hurt less.&lt;/p&gt;

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

&lt;p&gt;My context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I currently work on a 12-year-old Rails legacy code base&lt;/li&gt;
&lt;li&gt;The code base is undergoing modernization. Some of the large Active Record classes have been split into smaller ones, each into its own bounded context. Events are becoming a first-class citizens in the code. We also pay close attention to keep direction of dependencies as designed by context maps.&lt;/li&gt;
&lt;li&gt;The client has a GitHub Copilot subscription. I mostly use Sonnet and Opus models.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the_basic_setup"&gt;The basic setup&lt;/h2&gt;
&lt;p&gt;Initially I started with the basics. I was curious where it would get us. There&amp;rsquo;s an AGENTS.md file with general rules to follow. Besides the AGENTS.md file I&amp;rsquo;ve added a few skills.
The goal of the skills is to tell the agent about how it should write code. I am a big fan of &lt;a href="https://blog.arkency.com/test-which-reminded-me-why-i-dont-really-like-rspec/"&gt;Szymon&amp;rsquo;s way of using RSpec&lt;/a&gt;. So I put that into a skill. I also developed a few skills that tell the agent how I want it to deal with event sourcing, ddd technical patterns, hotwire, backfilling data (especially events) and mutation testing. The mutation skill is quite essential because without it the agent goes bananas and tries to achieve 100% of mutation coverage with hacking. &lt;/p&gt;

&lt;p&gt;An example of hacking is calling &lt;code&gt;send(:method)&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;I don&amp;rsquo;t want to have such tests. Trying to achieve mutation coverage in such a way indicates that perhaps the code should be removed because it&amp;rsquo;s just unnecessary noise.&lt;/p&gt;

&lt;p&gt;So now the question is, is that enough?&lt;/p&gt;
&lt;h2 id="it__39_s_pretty_good_but_can_be_better___tackling_non_determinism"&gt;It&amp;rsquo;s pretty good but can be better – tackling non-determinism&lt;/h2&gt;
&lt;p&gt;More than once (a day) I&amp;rsquo;ve experienced my agent to go off-rails and ignore my instructions. It doesn&amp;rsquo;t respect what I&amp;rsquo;ve specified in AGENTS.md and/or skills.&lt;/p&gt;

&lt;p&gt;It often happens when I am asking it to introduce a very similar-yet-a-little-bit-different command and handler for a specific business use case.&lt;/p&gt;

&lt;p&gt;Changes to the production code are going very well. This is especially true if the goal is to replicate well-structured code. However, once it gets to the &amp;ldquo;write the tests&amp;rdquo; part, it switches to commodity mode and most likely uses RSpec in the most popular way, which I don&amp;rsquo;t like. This is a large part of the existing codebase. If it doesn&amp;rsquo;t fail on writing tests the way I want it, it usually doesn&amp;rsquo;t run mutation testing, even though I expect the coverage not to drop below a certain point and the mutants to be eliminated. They should be killed properly.
Using the &lt;code&gt;send&lt;/code&gt; method is no bueno.&lt;/p&gt;
&lt;h3 id="dealing_with_non_determinism"&gt;Dealing with non-determinism&lt;/h3&gt;
&lt;p&gt;So we&amp;rsquo;re not able to change whether the agent will respect AGENTS.md and skills all the time. At least not yet. Maybe never. So we have to deal with it differently.&lt;/p&gt;

&lt;p&gt;What I am currently testing is to have guardrails aka dev workflows. The idea is to run tools that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Will make me focus less on code structure, incorrect formatting, etc&lt;/li&gt;
&lt;li&gt;Make sure tests for changed files are run&lt;/li&gt;
&lt;li&gt;Make sure mutation tests are run&lt;/li&gt;
&lt;li&gt;And, last but not least, make sure that the boundaries within bounded contexts are not violated. I noticed that the agent, just like humans, loves to take shortcuts to achieve a goal. The difference is that I never tell the agent we&amp;rsquo;re under a strict deadline. So I&amp;rsquo;m not sure where this choice is coming from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow is Ruby code that is wired to a &lt;code&gt;/verify&lt;/code&gt; custom command. The command runs bash with &lt;code&gt;ruby -r ./lib/dev_workflow.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dev_workflow.rb&lt;/code&gt; orchestrates the full pipeline. Looking at its requires tells you everything about what it runs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/step_result'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/result'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/changed_files'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/base'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/rubocop_step'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/rspec_step'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/mutant_step'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/eslint_step'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/steps/jest_step'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'dev_workflow/verify_build'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each step follows the same pattern: check if relevant files changed, run the tool, return a structured result. Here&amp;rsquo;s the mutation testing step as an example — the one that matters most given the problems I described earlier:&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;MutantStep&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="no"&gt;ALLOWED_NAMESPACES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[CRM Ordering Billing]&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="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any_ruby?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;skip_reason: &lt;/span&gt;&lt;span class="s1"&gt;'no ruby files changed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;subjects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutation_subjects&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;skip_reason: &lt;/span&gt;&lt;span class="s1"&gt;'no mutant-eligible files changed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measure_duration&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;run_mutant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;
      &lt;span class="no"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;duration_seconds: &lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;files_checked: &lt;/span&gt;&lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse_mutant_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;duration_seconds: &lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;files_checked: &lt;/span&gt;&lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="n"&gt;errors&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;run_mutant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subject_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&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;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;run_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bundle exec mutant run --since HEAD &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;subject_args&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;mutation_subjects&lt;/span&gt;
    &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ruby_files&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;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spec/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;file_to_subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;eligible_namespace?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniq&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 key detail is &lt;code&gt;StepResult&lt;/code&gt;. Each step returns either &lt;code&gt;.skipped&lt;/code&gt;, &lt;code&gt;.success&lt;/code&gt;, or &lt;code&gt;.failure&lt;/code&gt; with structured data. This is what the agent reads to understand what went wrong and what to fix.&lt;/p&gt;

&lt;p&gt;Last but not least, to make sure that the non-deterministic agent won&amp;rsquo;t ignore my desire to run this command by itself, I attached it to a git pre-commit hook:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env ruby&lt;/span&gt;

&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s2"&gt;"../lib/dev_workflow"&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DevWorkflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VerifyBuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;staged_only: &lt;/span&gt;&lt;span class="kp"&gt;true&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;

&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And at this point, at least calling the verify method is deterministic. So the agent gets feedback, fixes whatever is reported by the tool, reruns the verification and then it&amp;rsquo;s able to commit the changes.&lt;/p&gt;
&lt;h2 id="reviewing_changes"&gt;Reviewing changes&lt;/h2&gt;
&lt;p&gt;Besides AGENTS.md, SKILLS.md and the workflow I described above, I still review the code. I focus on tests, architecture and security parts.
I do take full ownership of the code that I ship. I don&amp;rsquo;t trust the AI enough to cut the leash. And my conclusion from working with it in a legacy codebase is currently that it will not change that fast (for me).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2026-02-05:/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://blog.arkency.com/the-timezone-bug-that-hid-in-plain-sight-for-months/" type="text/html"/>
    <content type="html">&lt;h1 id="the_timezone_bug_that_hid_in_plain_sight_for_months"&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;rsquo;s data sync that had been silently causing inconsistencies for months. The bug was elegant in its simplicity: checking DST status for &amp;ldquo;now&amp;rdquo; when converting historical dates.&lt;/p&gt;

&lt;!-- more --&gt;
&lt;h2 id="the_broken_code"&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;rsquo;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 id="why_this_breaks"&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 id="the_fix"&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 id="the_test_that_exposed_it"&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 id="impact"&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 id="lessons"&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;rsquo;t rely on your system&amp;rsquo;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:blog.arkency.com,2026-01-14:/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://blog.arkency.com/stop-using-datetime-in-2026-unless-you-work-for-unesco/" type="text/html"/>
    <content type="html">&lt;h1 id="stop_using_datetime_in_2026__unless_you_work_for_unesco_"&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;rsquo;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;ldquo;&lt;code&gt;DateTime&lt;/code&gt; handles a wider range of dates.&amp;rdquo;&lt;/p&gt;

&lt;p&gt;That was partially true. In 2008. On 32-bit systems.&lt;/p&gt;
&lt;h2 id="datetime__39_s_range_advantage_died_in_ruby_1_9_2"&gt;DateTime&amp;rsquo;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;rsquo;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 id="remember_rails_4_2_"&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;ldquo;fixing&amp;rdquo; 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;rsquo;s breaking change to &lt;code&gt;DateTime#utc&lt;/code&gt; wasn&amp;rsquo;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 id="the_unesco_problem"&gt;The UNESCO problem&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;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;rsquo;s own documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It&amp;rsquo;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;rsquo;t yet adopted the Gregorian Calendar Reform (and wouldn&amp;rsquo;t until 1752) their deaths are actually 10 days apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ruby&amp;rsquo;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;rsquo;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;rsquo;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 id="_code_datetime__code__s_actual_problems"&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;rsquo;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;rsquo;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 id="why_people_still_use_it"&gt;Why people still use it&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;We&amp;rsquo;ve always used it&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

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

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

&lt;p&gt;Use &lt;code&gt;Date&lt;/code&gt;. That&amp;rsquo;s what it&amp;rsquo;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;ldquo;The library I&amp;rsquo;m using returns DateTime&amp;rdquo;&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 id="what_to_use_instead"&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 id="the_only_exception"&gt;The only exception&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;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 id="references"&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;rsquo;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:blog.arkency.com,2025-12-30:/adding-multitenancy-to-a-ddd-rails-app/</id>
    <title type="html">Adding multi-tenancy to a DDD Rails app</title>
    <published>2025-12-30T11:00:00Z</published>
    <updated>2025-12-30T11:00:00Z</updated>
    <link rel="alternate" href="https://blog.arkency.com/adding-multitenancy-to-a-ddd-rails-app/" type="text/html"/>
    <content type="html">&lt;h1 id="adding_multi_tenancy_to_a_ddd_rails_app"&gt;Adding multi-tenancy to a DDD Rails app&lt;/h1&gt;
&lt;p&gt;Many businesses when they set out to create some software they need, don&amp;rsquo;t know that one day, they might need multi-tenancy.&lt;/p&gt;

&lt;p&gt;This is one of the features, that is not easy for programmers to add later easily. It might take months or even years. &lt;/p&gt;

&lt;p&gt;Let me describe how I approached this in an ecommerce app. Essentially, the idea is to allow to create multiple stores, where previously it was one (or actually lack of any, all resources were global).&lt;/p&gt;

&lt;!-- more --&gt;
&lt;h2 id="the_ecommerce_project"&gt;The ecommerce project&lt;/h2&gt;
&lt;p&gt;The project I am talking about is called &lt;a href="https://github.com/RailsEventStore/ecommerce"&gt;ecommerce&lt;/a&gt; and it is part of the RailsEventStore (RES) organization on github. It started as a sample application for RES but over the last 10 years it grew to some kind of utopian Rails/DDD/CQRS/Events project.&lt;/p&gt;

&lt;p&gt;This project does run on &lt;a href="https://ecommerce.arkademy.dev"&gt;production&lt;/a&gt;, but it&amp;rsquo;s not really a production project. It&amp;rsquo;s more of a visionary/educational project to show a Rails codebase that can be highly modular in a DDD fashion.&lt;/p&gt;

&lt;p&gt;In this project, there was no concept of a Store. All the main resources were global, as in &lt;code&gt;Order.all&lt;/code&gt; etc.&lt;/p&gt;

&lt;p&gt;Similarly, the events didn&amp;rsquo;t have any data or metadata that would point to a specific store.&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;OrderConfirmed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Another idea related to this whole project is that those events (and wider - domains/bounded contexts) are generic in their nature. As such they can be used in other apps without changes. &lt;/p&gt;

&lt;p&gt;Just to prove this point that the domains can be reused, another rails app (&lt;a href="https://github.com/RailsEventStore/ecommerce/tree/master/apps/pricing_catalog_rails_app"&gt;pricing_catalog_rails_app&lt;/a&gt;) exists which requires (as gems) the existing bounded contexts.&lt;/p&gt;
&lt;h2 id="the_obvious_solution___add_store_id_to_all_commands_events"&gt;The obvious solution - add store_id to all commands/events&lt;/h2&gt;
&lt;p&gt;When I talked to people, how they would approach adding multi-tenancy here this idea repeated. You need to extend existing events and commands with store_id.&lt;/p&gt;

&lt;p&gt;I don&amp;rsquo;t like such invasive approaches by default. &lt;/p&gt;

&lt;p&gt;Also, the domains reusability aspect - while still working, would be less elegant. Some event data would exist but never used in other apps (which don&amp;rsquo;t need multi-tenancy).&lt;/p&gt;

&lt;p&gt;It&amp;rsquo;s definitely a concept that would work - so if you don&amp;rsquo;t have such abstract needs as reusability, this may be the way to go.&lt;/p&gt;
&lt;h2 id="the_different_schema_approach"&gt;The different schema approach&lt;/h2&gt;
&lt;p&gt;Another obvious solution is to use some database concepts. Create a new schema per tenant or a new db. &lt;/p&gt;

&lt;p&gt;I also excluded this from my choices - I didn&amp;rsquo;t want to solve this at the infra level. It also wasn&amp;rsquo;t clear to me, how would I operate on some cross-store reports which are often required for such Shopify-like platforms.&lt;/p&gt;
&lt;h2 id="what_exactly_is_multi_tenancy_"&gt;What exactly is multi-tenancy?&lt;/h2&gt;
&lt;p&gt;For some time, I didn&amp;rsquo;t have an alternative solution either. I was contemplating what it means to be multi-tenant, tried to split into smaller concepts.&lt;/p&gt;
&lt;h3 id="filtering_data"&gt;Filtering data&lt;/h3&gt;
&lt;p&gt;We need to filter data. When a specific store is shown, we are displaying only the products of this store. In my architecture, that&amp;rsquo;s a read model job. Definitely, my read models would need to be extended by store_id concept. I was OK with that, even though it was an invasive change. In my book, read models are application specific and if such a big application requirement comes, the read models need to adjust.&lt;/p&gt;
&lt;h3 id="authorization"&gt;Authorization&lt;/h3&gt;
&lt;p&gt;We need to authorize access to data.&lt;/p&gt;

&lt;p&gt;This is where good old Rails controllers come handy. We need some concept of current_store and then pass the store_id to the read models.&lt;/p&gt;
&lt;h3 id="admin_panel"&gt;Admin panel&lt;/h3&gt;
&lt;p&gt;We need some admin panel, where stores can be created, deleted  and listed.
In my case that&amp;rsquo;s a new read model but also a new &amp;ldquo;namespace/route&amp;rdquo; in the Rails app. &lt;/p&gt;
&lt;h3 id="cqrs___write"&gt;CQRS - write&lt;/h3&gt;
&lt;p&gt;So far, we have discussed the reads parts. 
In our CQRS split between reads and writes - how do we handle writes?&lt;/p&gt;

&lt;p&gt;We have commands like this:&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;ProductCatalog&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegisterProduct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Command&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&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;Similarly as with events, the commands are part of the BCs and shouldn&amp;rsquo;t need to change.&lt;/p&gt;

&lt;p&gt;Still, we need some way of saying that this Product is registered within a store. There&amp;rsquo;s no way around it.&lt;/p&gt;

&lt;p&gt;How can we do it, without changing the existing command definitions?&lt;/p&gt;

&lt;p&gt;What I wanted was a solution that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;doesn’t change existing BC APIs&lt;/li&gt;
&lt;li&gt;keeps domains reusable&lt;/li&gt;
&lt;li&gt;keeps multi-tenancy domain and app level, not infra&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these constraints in mind, the solution I arrived at looks almost obvious in hindsight.&lt;/p&gt;
&lt;h2 id="my_solution"&gt;My solution&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m still polishing the edges here, but overall my attempt seems to work.&lt;/p&gt;

&lt;p&gt;The main idea is to create a new Bounded Context - &lt;code&gt;Stores&lt;/code&gt;. This is the home for a new kind of events. The events are tiny (I like them this way) and they are just registering the main resources within the Store.&lt;/p&gt;

&lt;p&gt;So, they look like this:&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;ProductRegistered&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&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;CustomerRegistered&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&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;OfferRegistered&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:offer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Infra&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UUID&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&amp;rsquo;s not really much logic around it, though. I did solve the problem of making this change non-invasive, but I do admit, the concept of such repetitive events is not super convincing either. &lt;/p&gt;

&lt;p&gt;So, yeah, that&amp;rsquo;s the drawback.&lt;/p&gt;

&lt;p&gt;But there are more things that I like here.&lt;/p&gt;

&lt;p&gt;First of all, None of other BCs had to change in any way. Maybe one of the existing process managers had to change to include Store registration.&lt;/p&gt;

&lt;p&gt;Plenty of read models had to change, but that was expected. They all need to subscribe to the one new event. They persist the store_id and they know how to filter.&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;event_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DraftOrder&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;to: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferDrafted&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;event_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AssignStoreToOrder&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;to: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Stores&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferRegistered&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;Orders&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssignStoreToOrder&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="nf"&gt;find_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;uid: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_id&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;
        &lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;store_id: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:store_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the controllers, we now need to filter and authorize data:&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;InvoicesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_invoice_in_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;current_store_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;not_found&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@invoice&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also, in the controller, when we &amp;ldquo;create&amp;rdquo; new resources, we issue two commands, one for the original BC, the other one for Stores BC:&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;CouponsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;coupon_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;:coupon_id&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;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;create_coupon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coupon_id&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;rescue&lt;/span&gt; &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Coupon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AlreadyRegistered&lt;/span&gt;
    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:notice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Coupon is already registered"&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"new"&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;coupons_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Coupon was successfully created"&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;create_coupon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coupon_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RegisterCoupon&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;coupon_id: &lt;/span&gt;&lt;span class="n"&gt;coupon_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;name: &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;:name&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:code&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;discount: &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;:discount&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="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Stores&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RegisterCoupon&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;coupon_id: &lt;/span&gt;&lt;span class="n"&gt;coupon_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;store_id: &lt;/span&gt;&lt;span class="n"&gt;current_store_id&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This coupling is intentional and happens only at the application boundary, not inside BCs.&lt;/p&gt;

&lt;p&gt;It was also a nice opportunity to revise all the 16 existing read models and make some long-needed cleanups too.&lt;/p&gt;
&lt;h2 id="how_claude_code_helped_me_here"&gt;How Claude Code helped me here&lt;/h2&gt;
&lt;p&gt;Hard to admit, but I wrote maybe 10% of the code changes in this whole implementation of multi-tenancy.&lt;/p&gt;

&lt;p&gt;I assisted Claude in the original read model change - &lt;code&gt;Orders&lt;/code&gt;. Then, I was shocked how well Claude worked with all other places. It knew the patterns and just repeated them. &lt;/p&gt;

&lt;p&gt;It wouldn&amp;rsquo;t work, though, if not for the mutation test coverage.&lt;/p&gt;

&lt;p&gt;Honestly, in all cases, when I followed the reasoning and the steps made by the AI, there were tiny hallucinations or tiny weird solutions, or tiny commenting out code.&lt;/p&gt;

&lt;p&gt;Which all was caught by &lt;a href="https://github.com/mbj/mutant"&gt;mutant&lt;/a&gt;, the main quality guard I have here against AI.&lt;/p&gt;

&lt;p&gt;If not for mutant, I&amp;rsquo;d have to be more in control and the constant code reviews would drive me crazy. With mutant in place I was much more confident - and faster!&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;m gonna write more about this AI experience in other blogposts. This story is already a bit weird - starting from DDD, via events, to multi-tenancy, to read models, to Claude Code. Thanks for bearing with me here. &lt;/p&gt;

&lt;p&gt;AI was crucial here, though. If not for AI, I&amp;rsquo;d hate myself by the 3rd of the 16 read models with the boring repetitive work.&lt;/p&gt;

&lt;p&gt;If not for mutant, I&amp;rsquo;d hate myself for verifying AI in all 16 modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To be honest, I don&amp;rsquo;t know how people work with agents without mutation testing coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without mutant it would be like working with juniors with short attention span. Actually, agents are now senior level sometimes, but it&amp;rsquo;s seniors with dementia.&lt;/p&gt;

&lt;p&gt;Here is an example mutant output, when AI worked on the &lt;code&gt;Shipments&lt;/code&gt; read model. It ran mutant by itself, as it knows (CLAUDE.md) that it is required.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;progress: 440/447 alive: 0 runtime: 34.21s killtime: 123.81s mutations/s: 12.86
Mutant environment:
Usage:           opensource
Matcher:         #&amp;lt;Mutant::Matcher::Config ignore: [] subjects: [Shipments*]&amp;gt;
Integration:     minitest
Jobs:            4
Includes:        ["test"]
Requires:        ["./config/environment"]
Operators:       light
MutationTimeout: 10
Subjects:        11
All-Tests:       359
Available-Tests: 359
Selected-Tests:  19
Tests/Subject:   1.73 avg
Mutations:       447
Results:         447
Kills:           447
Alive:           0
Timeouts:        0
Runtime:         34.73s
Killtime:        126.85s
Efficiency:      365.22%
Mutations/s:     12.87
Coverage:        100.00%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This was a successful run, but often it would catch its own hallucinations and thanks to mutant it fixed itself.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;To summarize - it&amp;rsquo;s still too early to evaluate the solution, I still need to finish some reviews. But it does seem promising to me and I&amp;rsquo;m happy with the outcome, despite the drawbacks.&lt;/p&gt;

&lt;p&gt;The key lesson for me wasn&amp;rsquo;t multi-tenancy itself, but that strong architectural foundation + mutation testing make large-scale AI-assisted refactors possible.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;Discuss this post on &lt;a href="https://x.com/andrzejkrzywda/status/2006210691271655673?s=20"&gt;X (Twitter)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2025-12-22:/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://blog.arkency.com/rewrite-with-confidence-validating-business-rules-through-isolated-testing/" type="text/html"/>
    <content type="html">&lt;h1 id="rewrite_with_confidence__validating_business_rules_through_isolated_testing"&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;rsquo;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;rsquo;t work. Full test coverage would take months we didn&amp;rsquo;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 id="the_context__insurtech_at_scale"&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;rsquo;t break things. We had to be 100% sure that the new flow provided the same outcome.&lt;/p&gt;
&lt;h2 id="the_architecture__a_rails_monolith_under_transformation"&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;rsquo;s no coincidence they became successful. Over the past few years, they&amp;rsquo;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 id="understanding_the_problem"&gt;Understanding the Problem&lt;/h2&gt;&lt;h3 id="the_god_model"&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;ldquo;We don&amp;rsquo;t want &lt;code&gt;pending&lt;/code&gt; and &lt;code&gt;stubbed&lt;/code&gt; Quotes in the system.&amp;rdquo; Pending represented abandoned quotes with no value. Stubbed meant the system couldn&amp;rsquo;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;rsquo;t pretend you don&amp;rsquo;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 id="the_data_complexity"&gt;The Data Complexity&lt;/h3&gt;
&lt;p&gt;The Quote model contained two serialized columns with deeply nested data. Here&amp;rsquo;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 id="why_traditional_approaches_failed"&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;rsquo;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 id="what_about_other_approaches_"&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;rsquo;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 id="the_solution__testing_on_production"&gt;The Solution: Testing on Production&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;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 id="the_brave_new_flow"&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;rsquo;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 id="implementing_the_sampling_mechanism"&gt;Implementing the Sampling Mechanism&lt;/h3&gt;
&lt;p&gt;We built a sampling system using Ruby&amp;rsquo;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 id="what_we_sampled"&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 id="recording_http_interactions"&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 id="the_verification_process"&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 id="leaving_no_trace"&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 id="after_commit_handling"&gt;After Commit Handling&lt;/h3&gt;
&lt;p&gt;This feature became built-in to Rails 7.2, but we weren&amp;rsquo;t there yet. Fortunately, one of the best things about working at Arkency is that if you need a solution, there&amp;rsquo;s a good chance we&amp;rsquo;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 id="http_stubbing_strategy"&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 id="handling_edge_cases"&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;rsquo;t easy to stub. For AWS S3 clients, we used Ruby&amp;rsquo;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;rsquo;t mutate state).&lt;/p&gt;
&lt;h3 id="the_complete_verification_flow"&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 id="comparing_results"&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;rsquo;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 id="the_results"&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;ldquo;This is part of one of the best releases I have ever experienced.&amp;rdquo;&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 id="key_takeaways"&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;rsquo;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 id="prefer_watching_"&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;rsquo;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>
</feed>

