<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kyle Smith's blog]]></title><description><![CDATA[Kyle Smith's blog]]></description><link>https://kylecoding.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 12:10:29 GMT</lastBuildDate><atom:link href="https://kylecoding.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Sharing My Toys]]></title><description><![CDATA[Every now and then, I’ll come up with an idea for an app or code up a small experiment. I’ve considered sharing those publicly, but for any that are a full web app, I’d have to register a domain and spin up a new server for each one. Not only does th...]]></description><link>https://kylecoding.com/sharing-my-toys</link><guid isPermaLink="true">https://kylecoding.com/sharing-my-toys</guid><category><![CDATA[Rails]]></category><category><![CDATA[rails-engines]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[vps]]></category><category><![CDATA[server hardening]]></category><category><![CDATA[git-submodule]]></category><category><![CDATA[side project]]></category><category><![CDATA[cost-optimisation]]></category><category><![CDATA[deployment strategies]]></category><dc:creator><![CDATA[Kyle Smith]]></dc:creator><pubDate>Tue, 30 Dec 2025 15:42:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qBrF1yu5Wys/upload/1e5bd9842bc023e59976444d31ac5d0b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every now and then, I’ll come up with an idea for an app or code up a small experiment. I’ve considered sharing those publicly, but for any that are a full web app, I’d have to register a domain and spin up a new server for each one. Not only does that require significant time, it could also get expensive pretty quickly, and I don’t want to spend a bunch of money on my side projects.</p>
<p>Some of them I could set up on GitHub Pages if they’re just a JavaScript demo, which could be a good option, but I’d prefer to host them on my own website.</p>
<p>I’ve been working on a solution to this on and off for the past several months, and was recently able to finish up a first version. My hope is that reducing the friction and cost to getting these side projects out there will also help me maintain motivation to work on them.</p>
<p>This article will cover my current solution, including some things I tried along the way that didn’t work out, and where I might take this setup in the future.</p>
<h1 id="heading-the-goals">The Goals</h1>
<p>Going into this project, I knew:</p>
<ol>
<li><p>I want to build with Ruby on Rails. That’s my primary environment at work, and I don’t want to spend time learning a new framework. It also makes it easy to get something up and running quickly, which is a plus when I don’t have a lot of time to put towards these projects.</p>
</li>
<li><p>I already have a domain registered (this one), so I want to deploy to subdomains of that. This also has the benefit that I don’t need to put much effort into naming any of these apps, at least initially.</p>
</li>
<li><p>To keep hosting costs down, I only want to run a single, inexpensive server. If any app gets enough traffic that it needs a dedicated server, it’ll be time to split it out to its own domain.</p>
</li>
<li><p>I want it to be easy to add a new app to the setup.</p>
</li>
</ol>
<p>One other goal was to open source some of these apps and experiments. I’ve been working in software over ten years now, but I haven’t often shared my learning outside my company because nearly all my work is proprietary. I’ve benefited greatly from open source software, both by pulling it in as dependencies and from reading the source, and I hope others can benefit similarly from these projects. Part of the reason I started this blog was so that I could share what I’ve learned with the broader software community.</p>
<p>My initial thought was to create Rails engines as semi-isolated guest apps, and mount multiple of those within one host Rails app. After a good bit of work, that is the solution I ended up with, though there were many problems to solve along the way and a few dead ends.</p>
<h1 id="heading-just-give-me-the-solution-already">Just give me the solution already!</h1>
<p>You can check out <a target="_blank" href="https://toybox.kylecoding.com">the live app here</a>, and <a target="_blank" href="https://github.com/kylesmile/toybox-host-example">find the code on GitHub</a>. The GitHub repository is not exactly the same as what I have deployed, because I’m not sharing all of the apps publicly. It does have all the important bits, though. The README doesn’t go deep into the details of how everything works (that’s what this post is for), but it should be enough to get you started.</p>
<p>I’m calling this “Toybox” after the concept of Breakable Toys from the book <a target="_blank" href="https://www.amazon.com/Apprenticeship-Patterns-Guidance-Aspiring-Craftsman/dp/0596518382"><em>Apprenticeship Patterns</em> by Dave H. Hoover and Adewale Oshineye</a>. I read that book early in my software development journey, and that pattern stood out to me at the time, enough that I still remember it now. For this initiative, I’m building small apps and experiments with no commitment to continue development. Some may eventually become full apps, while others may never get beyond a demo or proof of concept. The point is creating and sharing them.</p>
<p>I’ve created one public app initially: a simple reading tracker app that doesn’t feel like it needs to be a social network. Seriously, why <em>are</em> so many reading trackers also a social network?! The source is <a target="_blank" href="https://github.com/kylesmile/toybox-books">also available on GitHub</a>. There are a few more features I’d like to build for that app, and I have ideas for a few other public apps. Both will come whenever I have enough free time to work on them.</p>
<h1 id="heading-hows-all-of-this-work">How’s all of this work?</h1>
<p>With that background out of the way, I’ll use the rest of this article to talk about the details of my setup.</p>
<h2 id="heading-hosting">Hosting</h2>
<p>I’m running this on a $6/month DigitalOcean VPS (1 vCPU, 1 GB RAM, 25 GB disk), and I’m using the lowest tier (~$15/month) of their managed PostgreSQL database (1 vCPU, 1 GB RAM, 10 GB storage). The total cost works out to just over $20/month. I could get that lower if I ran my own database server, but I’m happy with it for now. It’s possible this could be managed on the $4/month server, but 512 MB RAM would be tight. Maybe if I wasn’t using Docker?</p>
<h2 id="heading-deploy">Deploy</h2>
<p>I use Kamal 2.0 to build the Docker container and deploy the app. I found it has a pretty steep learning curve, particularly for anything more advanced, but once it’s set up it makes deploying easy. It does not handle server hardening, though. I followed some of DigitalOcean’s tutorials to lock everything down, including these steps:</p>
<ul>
<li><p>Disabled password logins over SSH.</p>
</li>
<li><p>Created a non-root user for deploys and SSH access.</p>
<ul>
<li><p>The deploy user can use <code>sudo</code>, but a password is required.</p>
</li>
<li><p>Disabled <code>root</code> as an SSH user.</p>
</li>
<li><p>Added the deploy user to the <code>docker</code> group so Kamal has the required permissions to deploy.</p>
</li>
</ul>
</li>
<li><p>Blocked all incoming connections except HTTP, HTTPS, and SSH with <code>ufw</code>.</p>
</li>
<li><p>Ran system and package updates.</p>
</li>
<li><p>Enabled automatic updates with <code>unattended-upgrades</code>.</p>
</li>
</ul>
<p>I initially had the guest app engines in a sibling folder of the host app, which didn’t work well with Kamal because it clones the git repository to a temp folder by default, though there is an option to use the current directory as the build context instead. However, that setup also caused challenges when building the Dockerfile, because Docker doesn’t let you pull in folders from a parent directory. I wanted the guest apps to be their own git repositories to make it easier to share them individually or split them out later.</p>
<p>I tried several different approaches: build in a parent folder, copy the engine folders into place temporarily, symlink the folders, and probably a few more and variations of each. Eventually, I realized that git submodules were the right tool for the job. That way, the engines are their own repositories, but I can still put them in a subfolder of the host app. Exactly what I needed!</p>
<p>While I was working on this setup, Kamal got a new feature to use a local Docker registry with an SSH tunnel to send the docker image to the server during the deploy. I switched to that soon after it came out. It’s worked great for my single-server deploy and saves on Docker registry costs.</p>
<p>Also in Kamal’s config, I set up all the app credentials as ENV variables (stored in 1Password). I prefer an ENV approach to checking an encrypted file into source control. I also set up a Valkey accessory as the Action Cable backend. I considered using Solid Cable, but had issues with that and removed it before I’d fully figured out the database setup, but stuck with the Valkey/Redis backend because it should scale better and be a little more efficient.</p>
<p>I made some minor customizations to the Dockerfile and build setup to read the Ruby version from the <code>.ruby-version</code> file and setup and install Node, but not much other than that. Most likely, there are some improvements and optimizations to be found there still, particularly around caching to speed up the build.</p>
<h2 id="heading-guest-apps">Guest Apps</h2>
<p>Guest apps are Rails engines managed as git submodules. This introduces a little bit of friction to setting up an app, but nothing too crazy and it solves a lot of other problems with Kamal and the Dockerfile. Removing an engine takes several steps to remove the submodule, and I may create a script at some point to streamline that. I created a generator template that adds all the necessary integration points to get a guest app off the ground quickly.</p>
<p>Initially, I had the host app directly mounting the engines in <code>config/routes.rb</code>, but then 37Signals released Fizzy and I looked at their source code and saw they had an engine that registered an initializer to mount itself. I adopted that pattern and added a series of initializers to make the engines self-mount and self-configure.</p>
<p>That became the guiding principle for the guest app engines: if possible, all that should be required to set one up is to add the git submodule in the right folder (it’s a Rails app, after all, and Rails devs like convention over configuration). Other than the pesky <code>package.json</code> file, that is all that is needed.</p>
<p>The Gemfile is plain Ruby, so it can look in the right folder and add all the engines dynamically. I created a few helpers in <code>lib/engine_list.rb</code> so any Ruby files (or ERB for YAML config files) that need the list of engines can pull from a centralized location. There is one place for the JavaScript that needs to get the list of engines, but that has to have its own code.</p>
<p>I added a custom config option to the host app which facilitates the engine self-configuration: <code>root_host</code>, which in production is <code>toybox.kylecoding.com</code>, and in development is <code>toybox.kylecoding.localhost:3000</code>. Using <code>localhost</code> as a top-level domain (supported by all modern browsers as long as you add <code>http://</code> to the beginning, and I think only Safari requires that) allows running with the subdomain setup in development.</p>
<p>I also had to set <code>config.action_dispatch.tld_length = 2</code> since everything is a subdomain off of <code>toybox.kylecoding.com</code>, not just directly off of <code>kylecoding.com</code>.</p>
<h2 id="heading-javascript">JavaScript</h2>
<p>Originally, I was trying to go without a JavaScript build, but landed on having a minimal build for a few reasons:</p>
<ul>
<li><p>Rails doesn’t have built-in support for multiple separate importmaps. I found one article about how to set up isolated importmaps, and it involved copying a lot of Rails code. That didn’t seem like a good solution to me.</p>
</li>
<li><p>I could do a single importmap, but then the JS code gets all mixed together and I have to directly add guest app JS dependencies to the host app. Also not great.</p>
</li>
<li><p>If the engines each have a <code>package.json</code> file, and I can install them into the host app as JS packages, they can specify their own dependencies. Then the package manager (<code>yarn</code> in my case) can coordinate the versions of dependencies between each app so that they (hopefully) stay on the same version if multiple use the same dependency (reducing Docker image size). At the very least, I can have <code>yarn</code> deduplicate dependencies if I need to.</p>
</li>
</ul>
<p>The biggest challenge is that I can’t make <code>package.json</code> dynamic. I looked at a few different options for JS package managers, and all of them had this limitation. I settled on Yarn 4 because it has a <a target="_blank" href="https://yarnpkg.com/protocol/portal">portal: protocol</a> for installing packages from a local folder. Unlike the <code>file:</code> protocol, <code>portal:</code> references the local folder directly instead of copying it, which is perfect for this use.</p>
<p>To solve <code>package.json</code> being a static file, I created a script (<code>bin/update_engine_node_modules</code>) which adds and removes engines from <code>package.json</code> based on what’s in the <code>engines/</code> folder. Other than running <code>bundle install</code> and <code>yarn install</code>, that is the <em>one</em> manual step that needs to happen when adding or removing an engine. At some point, I’ll probably create a script that combines all the steps, including creating the git submodule.</p>
<p>I used <code>esbuild</code> to bundle the JavaScript. It creates a separate entry point for the host app and each guest app, using the list of folders in <code>engines/</code> (since I can write the config in a <code>.js</code> file).</p>
<p>The guest apps also define a constant with some additional information the host app needs to show a list of apps. This is just basic things like whether the engine should be public or not, a title and description, and a link to the source code.</p>
<h2 id="heading-gemfile-and-yaml-config-files">Gemfile and YAML config files</h2>
<p>The Gemfile is just a Ruby DSL, so I can dynamically add the guest app engines.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Gemfile</span>
require_relative <span class="hljs-string">"lib/engine_list"</span>
Toybox::ENGINE_LIST.each <span class="hljs-keyword">do</span> <span class="hljs-params">|engine|</span>
  gem engine, <span class="hljs-symbol">path:</span> File.expand_path(engine, Toybox::ENGINES_DIRECTORY)
<span class="hljs-keyword">end</span>
</code></pre>
<p>All the YAML config files that need the list of engines support ERB, including Kamal’s <code>config/deploy.yml</code>, so I could make all of those dynamic, too.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Sample from config/database.yml</span>

<span class="hljs-comment"># ERB still processes things in YAML comments.</span>
<span class="hljs-comment"># Putting this in a comment makes some editors' syntax highlighting happier.</span>
<span class="hljs-comment"># &lt;% require_relative "../lib/engine_list" %&gt;</span>
<span class="hljs-attr">default:</span> <span class="hljs-meta">&amp;default</span>
  <span class="hljs-attr">adapter:</span> <span class="hljs-string">postgresql</span>
  <span class="hljs-attr">encoding:</span> <span class="hljs-string">unicode</span>
  <span class="hljs-attr">pool:</span> <span class="hljs-number">100</span>

<span class="hljs-attr">development:</span>
  <span class="hljs-attr">primary:</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*default</span>
    <span class="hljs-attr">database:</span> <span class="hljs-string">toybox_development</span>
<span class="hljs-comment"># &lt;% Toybox::ENGINE_LIST.each do |engine_name| %&gt;</span>
  &lt;%=<span class="ruby"> engine_name </span>%&gt;<span class="hljs-string">:</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*default</span>
    <span class="hljs-attr">database:</span> <span class="hljs-string">toybox_&lt;%=</span> <span class="hljs-string">engine_name</span> <span class="hljs-string">%&gt;_development</span>
    <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/&lt;%=</span> <span class="hljs-string">engine_name</span> <span class="hljs-string">%&gt;_migrate</span>
<span class="hljs-comment"># &lt;% end %&gt;</span>
</code></pre>
<p>The same works for defining the hosts in <code>config/deploy.yml</code> (Kamal does not support automatic TLS certificate management for wildcard domains, so I need to specify each separately):</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Sample from config/deploy.yml</span>

<span class="hljs-comment"># &lt;% require_relative "./lib/engine_list" %&gt;</span>
<span class="hljs-attr">proxy:</span>
  <span class="hljs-attr">ssl:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">toybox.kylecoding.com</span>
    <span class="hljs-comment"># &lt;% Toybox::ENGINE_LIST.each do |engine_name| %&gt;</span>
    <span class="hljs-bullet">-</span> &lt;%=<span class="ruby"> engine_name.dasherize </span>%&gt;<span class="hljs-string">.toybox.kylecoding.com</span>
    <span class="hljs-comment"># &lt;% end %&gt;</span>
</code></pre>
<h2 id="heading-database">Database</h2>
<p>I’m using DigitalOcean’s managed PostgreSQL. I’d initially planned to prefix the table names for each guest app, but then I discovered that, unlike other managed PostgreSQL databases I’ve used in the past (Heroku), DigitalOcean’s offering is a managed PostgreSQL <em>database cluster</em>. The distinction is that DigitalOcean allows you to create multiple databases on a single database server, which is perfect for this use case. Once I figured that out, I abandoned the table name prefixes in favor of multiple databases. That makes the implementation cleaner in the short term, and should simplify splitting out one of the engines to its own app in the future.</p>
<p>It took me a while to get the multi-DB setup right. I started with just calling <code>connects_to</code> in the engine <code>ApplicationRecord</code> classes, then realized I needed Action Text and potentially Active Storage for at least one app, and that it would be good to have those isolated, too. I spent a good bit of time trying <code>activerecord-tenanted</code> before I realized that it assumes all databases have the same schema. In the end, I just used Rails’ built-in multi-DB and shard switching support, which <code>activerecord-tenanted</code> uses under the hood, so the time experimenting with that wasn’t wasted.</p>
<p>I ran <code>bin/rails generate active_record:multi_db</code> to bootstrap the config, then customized to suit my needs. In the host app, I set up the shard resolver to use the request subdomain and configured the available shards on <code>ActiveRecord::Base</code> so they would get picked up by the Action Text and Active Storage models.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/multi_db.rb</span>
Rails.application.configure <span class="hljs-keyword">do</span>
  config.active_record.shard_selector = { <span class="hljs-symbol">lock:</span> <span class="hljs-literal">true</span> }
  <span class="hljs-comment"># Select the database based on the app subdomain</span>
  config.active_record.shard_resolver = -&gt;(request) { request.subdomain.underscore }
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># Configure all "shards" at the top level so Action Text and Active Storage tables can be isolated.</span>
<span class="hljs-comment"># The shard resolver above handles switching to the right database.</span>
ActiveRecord::Base.connects_to <span class="hljs-symbol">shards:</span> {
  **Toybox::ENGINE_LIST.each_with_object({}) <span class="hljs-keyword">do</span> <span class="hljs-params">|engine_name, acc|</span>
    name_symbol = engine_name.to_sym
    acc[name_symbol] = { <span class="hljs-symbol">writing:</span> name_symbol, <span class="hljs-symbol">reading:</span> name_symbol }
  <span class="hljs-keyword">end</span>
}
</code></pre>
<p>Next, each engine <code>ApplicationRecord</code> class is configured to connect to a single shard. This may not be <em>strictly</em> necessary to run the app, but it makes interacting with the models in a Rails console much easier because they will automatically connect to the right database. This configuration is auto-generated by the template for new guest apps.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Books guest app engine: app/models/books/application_record.rb</span>
<span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Books</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationRecord</span> &lt; ApplicationRecord</span>
    <span class="hljs-keyword">self</span>.abstract_class = <span class="hljs-literal">true</span>

    connects_to <span class="hljs-symbol">shards:</span> {
      <span class="hljs-symbol">books:</span> { <span class="hljs-symbol">writing:</span> <span class="hljs-symbol">:books</span>, <span class="hljs-symbol">reading:</span> <span class="hljs-symbol">:books</span> }
    }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-host-app">Host app</h2>
<p>I tried to keep the host app relatively simple: one static page with some information on the project and a list of all the public guest apps (generated dynamically based on what guest app engines are present and the title/description provided by each engine). I don’t anticipate any major changes to the host app at this point, other than potentially handling the configuration for Active Job whenever I need it.</p>
<h2 id="heading-testing">Testing</h2>
<p>The host app doesn’t really have any tests at the moment, and I don’t plan to add any right now, since I don’t expect it to have a lot of changes, and it’s pretty simple.</p>
<p>Testing for the guest apps took a bit of effort to set up. I decided to stick with Minitest, although I’m more familiar with RSpec. I started by getting the tests from the authentication generator passing. For the most part, that was a matter of making sure everything (including fixture YAML files) was properly namespaced and the correct subdomain was set during the test.</p>
<p>For system tests, I had to set up a JS build within the engines, and make sure Slim was loaded for templating (you won’t convince me to use ERB). I also had to configure Capybara to use the correct subdomain and automatically add the port number. I didn’t find a good way to get the port number out of Capybara to configure the allowed hosts, so I disabled host authorization for test mode. I opted to use the Playwright driver, since I’ve had good experience with that in the past. It is the best I’ve tried for element interactions, and it works well with web components—much better than Selenium that fails pretty quickly when it has to deal with the shadow DOM. I don’t have any web components in my app right now, but they are my tool of choice when I need a JS-heavy interaction that’s more than what I want to do with Stimulus.</p>
<p>For the Books app, I focused on writing a few high-level system tests to get that ready to ship. It needs some more unit tests, which I’ll add as I’m building new features.</p>
<h2 id="heading-terms-of-use-and-privacy-policies">Terms of Use and Privacy Policies</h2>
<p>As a final step before I started sharing links to <code>toybox.kylecoding.com</code> and the related guest apps, I wanted to get some basic terms of use and privacy policies in place. I looked around to see if there were any good templates, and found most of the ones that claimed to be “free” wanted you to pay as soon as you needed terms to cover user sign ups.</p>
<p>Since this is a side project that I don’t want to spend a bunch of money on, I turned to ChatGPT to generate those legal documents, which I think are good enough for now and better than having nothing. I created separate ones for the host app and the Books app, since the host app is just a static page and doesn’t have user accounts or user-entered data.</p>
<h2 id="heading-whats-next">What’s next?</h2>
<p>Eventually, I’ll need to figure out background jobs, and I’ll want to set up some basic analytics. I’ll probably also tune the log messages to be less verbose in production.</p>
<p>Copying migrations from the engines could use some attention. The <code>bin/rails &lt;engine_name&gt;:install:migrations</code> commands technically work, but I haven’t figured out how to get it to be aware of the multi-database setup and put the migrations in the correct folder.</p>
<p>I’ll be keeping an eye on <code>Ruby::Box</code>, introduced in Ruby 4. Once that’s more mature, maybe I can use it to run multiple full Rails apps with a single Rack config, while still keeping them isolated. That could be simpler and cleaner if it doesn’t require building each app as a Rails engine. Whether I switch to that or not will depend on how that impacts resource usage.</p>
<p>I’ve likely missed explaining some part of this setup. If you come across something that you’d like to know more about, feel free to leave a comment or start a discussion on one of the GitHub repositories. I’m happy to answer any questions when I have time.</p>
<p>I plan to keep developing the Books app, and I have a few JS experiments I’ve done that might be fun to share. I also have ideas for another app or two that I may add to this setup. As I move those forward, I will likely post about them here. Stay tuned!</p>
]]></content:encoded></item><item><title><![CDATA[Fix flaky RSpec tests fast with parallel_tests and marsh_grass]]></title><description><![CDATA[One of the hardest parts of debugging and fixing flaky RSpec tests is reproducing the failure reliably, or even reproducing it at all. Several years ago, an experiment at my company produced the marsh_grass gem. This gem provides several tools for de...]]></description><link>https://kylecoding.com/fix-flaky-rspec-tests-fast-with-paralleltests-and-marshgrass</link><guid isPermaLink="true">https://kylecoding.com/fix-flaky-rspec-tests-fast-with-paralleltests-and-marshgrass</guid><category><![CDATA[#rspec]]></category><category><![CDATA[flaky-tests]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Kyle Smith]]></dc:creator><pubDate>Fri, 28 Mar 2025 13:04:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2EMt3sUUTqg/upload/2b5c0afb10b249d6cb1e8d69015c0f0b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the hardest parts of debugging and fixing flaky RSpec tests is reproducing the failure reliably, or even reproducing it at all. Several years ago, an experiment at my company produced the <a target="_blank" href="https://github.com/rolemodel/marsh_grass">marsh_grass gem</a>. This gem provides several tools for debugging flaky tests, including the <code>repetitions</code> metadata, which will run a test several times in a row. I have found this particular feature useful, particularly on those tests that just refuse to fail consistently. Running the test several times can give an indication of the failure rate, so I can tell if there’s been an improvement.</p>
<p>Until recently, this would still be relatively slow. I’d usually want at least 100 repetitions, and running those sequentially could take several minutes. But this was the best option I had available. Until we set up <a target="_blank" href="https://github.com/serpapi/turbo_tests/">turbo_tests</a>. Now, I could run my entire test suite locally in parallel, which itself was a big win.</p>
<p>But then I realized something else: turbo_tests brings in <a target="_blank" href="https://github.com/grosser/parallel_tests">parallel_tests</a>, and that also lets you run <em>any</em> command in parallel. Which meant I could set a relatively low number of repetitions and run those repetitions multiple times in parallel. By default, it will run one process per CPU core. My computer has 10 CPU cores, so that means I can run my usual 100 repetitions in just over a tenth of the time.</p>
<p>Here’s how it works. When I’m debugging a flaky test, I’ll first add the <code>repetitions</code> metadata.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># flaky_spec.rb</span>

RSpec.describe <span class="hljs-string">'flakiness'</span> <span class="hljs-keyword">do</span>
  it <span class="hljs-string">'is arbitrarily flaky'</span>, <span class="hljs-symbol">repetitions:</span> <span class="hljs-number">10</span> <span class="hljs-keyword">do</span>
    expect(rand(<span class="hljs-number">0</span>...<span class="hljs-number">100</span>)).to be &lt; <span class="hljs-number">75</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Then, run it with parallel_tests.</p>
<pre><code class="lang-bash">RAILS_ENV=<span class="hljs-built_in">test</span> bundle <span class="hljs-built_in">exec</span> parallel_test -e <span class="hljs-string">"bundle exec rspec flaky_spec.rb:4"</span>
</code></pre>
<p>That’s a lot to remember, so I made a simple script that I put in my PATH (under the name <code>parallel</code>).</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">command</span>=<span class="hljs-string">"bundle exec rspec <span class="hljs-variable">$@</span>"</span>
RAILS_ENV=<span class="hljs-built_in">test</span> bundle <span class="hljs-built_in">exec</span> parallel_test -e <span class="hljs-string">"<span class="hljs-variable">$command</span>"</span>
</code></pre>
<p>Now when I want to run several repetitions of a flaky test in parallel, this is all I have to do:</p>
<pre><code class="lang-bash">parallel flaky_spec.rb:4
</code></pre>
<p>Pretty easy to remember, and much shorter to type! One word of caution: this doesn’t exit cleanly when interrupted. If your test is driving a browser and you don’t let the process complete normally, you could be left with several browser processes still running that you’ll need to go clean up manually. Other than that, it’s a big win for debugging those pesky flaky tests.</p>
]]></content:encoded></item><item><title><![CDATA[Don't fire your software developers yet]]></title><description><![CDATA[Software developers are obsolete! I’d never written a line of code before, and I was able to create an app in a week using AI!

As LLMs continue to improve, I have seen people make this kind of statement more and more. Often the one making the statem...]]></description><link>https://kylecoding.com/dont-fire-your-software-developers-yet</link><guid isPermaLink="true">https://kylecoding.com/dont-fire-your-software-developers-yet</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[AI-Generated Code]]></category><category><![CDATA[Future of AI]]></category><category><![CDATA[Future of Programming]]></category><category><![CDATA[Experience ]]></category><category><![CDATA[expertise]]></category><dc:creator><![CDATA[Kyle Smith]]></dc:creator><pubDate>Thu, 13 Mar 2025 12:55:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/M5tzZtFCOfs/upload/36c450a33f603cc948d262fd05b24160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Software developers are obsolete! I’d never written a line of code before, and I was able to create an app in a week using AI!</p>
</blockquote>
<p>As LLMs continue to improve, I have seen people make this kind of statement more and more. Often the one making the statement is incredulous at their software developer friends’ seeming lack of interest in AI. But I think there’s something that those who have “never written a line of code before” don’t realize.</p>
<p>I’m not just talking about the fact that software development is much more than the mere act of typing code, though that is certainly true. There has been enough discussion on that topic already that I don’t feel like I need to cover it in much detail here. Suffice it to say that creating software involves much more thinking and problem solving than it does typing. However, even if someone knows this, I think there is another reason that they might come to the above conclusion of just how much an LLM can really do: they do not have the experience in software development needed to gauge someone else’s skill level.</p>
<p>To explain what I mean, consider the <a target="_blank" href="https://en.wikipedia.org/wiki/Dreyfus_model_of_skill_acquisition">Dreyfus Model of Skill Acquisition</a>. We often reference this at my company because it is a helpful tool for understanding how a person’s thought processes and capabilities change as they gain experience, especially in high-skill professions like software development. According to this model, a person progresses through these stages as they learn:</p>
<ol>
<li><p>Novice</p>
</li>
<li><p>Advanced Beginner</p>
</li>
<li><p>Competent</p>
</li>
<li><p>Proficient</p>
</li>
<li><p>Expert</p>
</li>
</ol>
<p>Someone in the first couple of stages relies primarily on following rules and step-by-step instructions. As a person gains experience, they rely more and more on intuition, and do not need to consciously apply rules. Importantly, Experts do not merely work faster than a Novice or Advanced Beginner. The way they think is entirely different, and they are capable of much, much more.</p>
<p>An Advanced Beginner may be able to throw an app together quickly. So can an Expert, though for more complex domains. Unlike an Advanced Beginner, an Expert can also maintain, update, and expand that app over the course of several years, because they will build it in a way that allows for modification. This requires managing the inherent complexity of the domain through good code architecture while avoiding introduced complexity from bad architecture.</p>
<p>Many people never make it past Advanced Beginner in a given field. I think this is often the case for hobbies—one might not be able to put in the time needed to advance to higher levels (it really does take around 10,000 hours to become an Expert). But more relevant to the AI discussion is the other reason someone might fail to achieve the later stages: being blind to how much they have left to learn.</p>
<p>When someone reaches the Advanced Beginner stage, they have already learned a lot and are much more capable than when they were a Novice. The danger is that this can trick the Advanced Beginner into believing that their skills are greater than they really are. They might even believe that they are Experts, ceasing to learn because they think there is nothing left to learn. Even worse, though, is how that person is perceived by someone else who is a Novice, because <em>to a Novice, an Advanced Beginner looks like an Expert</em>.</p>
<p>And this, I think, is the mistake so many people make with AI. By their own admission, having “never written code”, they are Novices. They see an LLM writing code that they have neither the skill to write nor to understand, and conclude that the AI is an Expert, when in reality it most likely is not. Then they go post about it on social media.</p>
<p>LLMs still need to prove that they can maintain an app long-term, and do it well, without continually increasing the time and cost to add features. Proving this will take time, though we can get some early indication by evaluating the quality of their code, particularly for how well they do when the subject is not found in their training data (please stop extrapolating from todo apps). So far, the hype comes mostly from those without coding experience, while those with experience tend to find that LLMs still have significant gaps in their abilities.</p>
<p>If you try to replace your software developers entirely now, you could be left with an app that an LLM can’t maintain, and no humans to correct it. You could try keeping only your senior developers, but if AI never becomes capable enough to replace them, you’ll be stuck trying to keep increasingly rare and valuable resource. Experienced developers will be in short supply at any company that isn’t willing to invest in growing their own talent.</p>
<p>Now, I am not at all saying that LLMs are useless, but I do think the hype is unreasonably optimistic, and often pushed by people with something to sell. Those instances where someone who is not a software developer has used one to build a system are quite impressive, and I think there will be many cases where that is sufficient. I also think they are valuable tools in the hands of an experienced practitioner, and can boost productivity in many ways. On the other hand, I have personally seen them be a detriment to junior developers who do not yet have the experience to validate their output.</p>
<p>I think it’s clear that while LLMs have some impressive capabilities and will continue to transform software development, they still cannot entirely replace human expertise. Maybe they will one day, and maybe they’ll prove capable of the long-term thinking necessary to build sustainable systems. Personally, I think that day is still a long way off, if it comes at all, and the smart move is to continue to invest in capable humans. Though maybe that’s just what I want to believe because I enjoy software development. Either way, I don’t plan to let the computers have <em>all</em> the fun.</p>
]]></content:encoded></item></channel></rss>