<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Code - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/code/</link><description>Code - Tag - Tracy Atteberry</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>tracy@magicbydesign.com (Tracy Atteberry)</managingEditor><webMaster>tracy@magicbydesign.com (Tracy Atteberry)</webMaster><lastBuildDate>Fri, 20 Mar 2026 00:00:00 +0000</lastBuildDate><image><url>https://tracyatteberry.com/images/feed-icon.jpg</url><title>Code - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/code/</link></image><atom:link href="https://tracyatteberry.com/tags/code/" rel="self" type="application/rss+xml"/><item><title>MockOpenAI</title><link>https://tracyatteberry.com/portfolio/mockopenai/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/mockopenai/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/mockopenai/mockopenai_hero.jpg" referrerpolicy="no-referrer">
            </div><p>Building a gem.</p>
]]></description></item><item><title>Jojo</title><link>https://tracyatteberry.com/portfolio/jojo/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/jojo/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/jojo/landing_page.png" referrerpolicy="no-referrer">
            </div><p>Building a CLI for turning job applications into marketing campaigns. Jojo
helps you create personalized, engaging applications that stand out from the
crowd. It generates tailored cover letters, optimizes your resume for ATS, and
even creates a custom portfolio website for each application. With Jojo, you
can turn your job search into a marketing campaign that gets noticed.</p>
]]></description></item><item><title>Enshortener</title><link>https://tracyatteberry.com/portfolio/enshortener/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/enshortener/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/enshortener/screenshot.png" referrerpolicy="no-referrer">
            </div><p>Building a personal URL shortener deployable via SFTP on shared hosting.</p>
]]></description></item><item><title>How to use Claude to create a custom exercise program</title><link>https://tracyatteberry.com/posts/custom_exercise/</link><pubDate>Sat, 20 Dec 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/custom_exercise/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/custom_exercise/top.png" referrerpolicy="no-referrer">
            </div><h1 id="how-to-use-claude-to-create-a-custom-exercise-program">How to use Claude to create a custom exercise program</h1>
<p>Recently, I decided to get back into regular exercise after a long period of
inactivity. Rather than following a generic workout plan from a fitness app, I
wanted something personalized to my specific situation, equipment, and goals. I
turned to Claude to help me create a comprehensive four-week exercise
program. The results seem great so far.</p>
<p>Here&rsquo;s how to get an AI to design something that actually works for you.</p>
<h2 id="starting-with-the-right-prompt">Starting with the right prompt</h2>
<p>The key to getting a useful exercise program from Claude is being specific
about your situation. I provided details about:</p>
<ul>
<li>My current fitness level and recent activity history</li>
<li>My specific goals (rebuilding strength and cardio endurance)</li>
<li>Equipment I had available (resistance bands, household stairs, a yoga mat, and a Nintendo Switch with Ring Fit Adventure)</li>
<li>My constraints and needs (requiring detailed exercise descriptions since I&rsquo;m a structured exercise novice)</li>
</ul>
<p>Claude responded with a complete four-week program that included exercises,
nutrition guidance, progress tracking suggestions, and motivational tips. The
level of detail was just about right. Each exercise came with form instructions
and links to video demonstrations.</p>
<h2 id="the-workout-schedule">The workout schedule</h2>
<p>To get concrete, actionable guidance rather than generic advice, I asked for a
full 28-day calendar with specific day-of-week workouts:</p>
<ul>
<li>Monday, Wednesday, Friday: Strength training</li>
<li>Tuesday, Thursday: Light cardio and mobility work</li>
<li>Saturday: Optional gentle recovery</li>
<li>Sunday: Complete rest</li>
</ul>
<p>The program also showed clear progression across the four weeks. Week 1 started
with wall pushups and assisted squats to build a foundation. By Week 4, I was
doing incline pushups, unassisted bodyweight squats, and using resistance bands
for rows and shoulder presses.</p>
<p>Sometimes the exercises look a bit too easy, so I&rsquo;m interested to see how this progressive schedule will work in practice.
And I can always adjust the plan later if needed.</p>
<h2 id="from-text-to-calendar">From text to calendar</h2>
<p>The written workout schedule (and exercise descriptions, and diet advice, etc)
was great, but I wanted something I could actually put in my calendar. I asked
Claude to generate an <code>.ics</code> file that I could import into my calendar app.</p>
<p>Claude&rsquo;s first attempt had a bug. It skipped December 25th (Christmas Day),
which threw off all the subsequent dates. To avoid this kind of error and make
things easier to test, fix, and update later, I asked Claude create a script to
generate the calendar instead of manually coding each event.</p>
<p>This worked out well for a few reasons:</p>
<ol>
<li>The script was much easier to debug and verify</li>
<li>I now have a reusable tool that I can modify if I want to adjust the schedule or create future workout programs</li>
<li>The initial script had a small bug, but it was easy to fix through an iterative conversation with Claude.</li>
</ol>
<p>Here&rsquo;s what the core of the calendar generation script looks like:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-python">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_event</span><span class="p">(</span><span class="n">date</span><span class="p">,</span> <span class="n">day_of_week</span><span class="p">,</span> <span class="n">week_num</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="s1">&#39;Home&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Create an iCalendar event string&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">week_key</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">&#39;week</span><span class="si">{</span><span class="n">week_num</span><span class="si">}</span><span class="s1">&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">workout</span> <span class="o">=</span> <span class="n">WORKOUTS</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">day_of_week</span><span class="p">,</span> <span class="p">{})</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">week_key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">workout</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">event</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;BEGIN:VEVENT&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">workout</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;all_day&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># All-day event for rest days</span>
</span></span><span class="line"><span class="cl">        <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;DTSTART;VALUE=DATE:</span><span class="si">{</span><span class="n">format_date</span><span class="p">(</span><span class="n">date</span><span class="p">)</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;DTEND;VALUE=DATE:</span><span class="si">{</span><span class="n">format_date</span><span class="p">(</span><span class="n">date</span><span class="p">)</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Timed event with duration</span>
</span></span><span class="line"><span class="cl">        <span class="n">start_hour</span> <span class="o">=</span> <span class="mi">10</span> <span class="k">if</span> <span class="n">day_of_week</span> <span class="o">==</span> <span class="s1">&#39;saturday&#39;</span> <span class="k">else</span> <span class="mi">9</span>
</span></span><span class="line"><span class="cl">        <span class="n">start_time</span> <span class="o">=</span> <span class="n">date</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">hour</span><span class="o">=</span><span class="n">start_hour</span><span class="p">,</span> <span class="n">minute</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">duration</span> <span class="o">=</span> <span class="n">workout</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;duration&#39;</span><span class="p">,</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">end_time</span> <span class="o">=</span> <span class="n">start_time</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">duration</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;DTSTART:</span><span class="si">{</span><span class="n">format_datetime</span><span class="p">(</span><span class="n">start_time</span><span class="p">)</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;DTEND:</span><span class="si">{</span><span class="n">format_datetime</span><span class="p">(</span><span class="n">end_time</span><span class="p">)</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Add event details</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;SUMMARY:</span><span class="si">{</span><span class="n">workout</span><span class="p">[</span><span class="s2">&#34;summary&#34;</span><span class="p">]</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;DESCRIPTION:</span><span class="si">{</span><span class="n">workout</span><span class="p">[</span><span class="s2">&#34;description&#34;</span><span class="p">]</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">event</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The script organizes workouts in a dictionary structure, making it easy to
modify individual workouts without touching the calendar generation logic. Each
day&rsquo;s workout includes the exercises, duration, and detailed descriptions that
show up in my calendar. It&rsquo;s not perfect and it lacks some features and finesse,
but for quick generation that&rsquo;s easy to tweak, it&rsquo;s exactly what I needed.</p>
<p>When I tried the initial script, I noticed it was using a deprecated
Python function (<code>datetime.utcnow()</code>). After the issue was mentioned,
Claude corrected it by switching to the modern timezone-aware
approach:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-python">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Old (deprecated)</span>
</span></span><span class="line"><span class="cl"><span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># New (timezone-aware)</span>
</span></span><span class="line"><span class="cl"><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span></span></span></code></pre></div></div>
<p>I&rsquo;m not sure why it wasn&rsquo;t correct in the first place, since Claude did run and
test the code before sharing it with me. Perhaps because it was &ldquo;only&rdquo; a warning?
In any case, Claude is pretty good at iterative improvement and this was quickly
resolved.</p>
<h2 id="what-worked-well">What worked well</h2>
<h3 id="personalization">Personalization</h3>
<p>The more specific I was about my context, the better the recommendations. If
you want to the AI to tailor something specific for you, then has to know some
specifics <em>about</em> you. Tell it about your equipment, experience level, and
goals.</p>
<h3 id="detailed-instructions">Detailed instructions</h3>
<p>As someone without much fitness experience, I needed more than just exercise
names. The AI provided form basic instructions, video links, and safety notes.</p>
<h3 id="progress-tracking">Progress tracking</h3>
<p>Ask for suggestions around measuring improvements beyond just body weight. Like
how many reps of an exercise to target or long to do things like holding a
plank.</p>
<h3 id="nutrition-guidance">Nutrition guidance</h3>
<p>If you&rsquo;re looking to build muscle, then protein targets based on weight will
help. Practical examples of how to hit those numbers throughout the day is also
nice.</p>
<h3 id="code-as-a-deliverable">Code as a deliverable</h3>
<p>Getting a Python script rather than just a static file means I have a tool I
can reuse and modify. If I want to adjust the workout times or change which
days are rest days, I can do that easily.</p>
<h2 id="what-could-be-better">What could be better</h2>
<p>The experience wasn&rsquo;t perfect. A few observations:</p>
<p><strong>Video links sometimes incorrect</strong></p>
<p>The form instruction videos Claude linked to are
YouTube videos that weren&rsquo;t always for the exact exercise that was recommended.
Fortunately, the written descriptions were often good enough to figure things out.</p>
<p><strong>Claude on the web doesn&rsquo;t do Ruby</strong></p>
<p>While I asked for a Ruby script to
generate the calendar, Claude on the web only supports Python for
something it can test. In fact, it insisted on using Python when it couldn&rsquo;t
get Ruby to install in its sandbox. Not a big deal, but something to be aware
of if you prefer a specific programming language.</p>
<h2 id="the-bigger-picture">The bigger picture</h2>
<p>This experience taught me that Claude can be genuinely useful for creating
structured, personalized plans in domains where you need expertise but don&rsquo;t
have it yourself. The key is to:</p>
<ol>
<li>Be specific about your situation and constraints</li>
<li>Ask for deliverables in formats you can actually use (calendar files, scripts, documents)</li>
<li>Iterate when something isn&rsquo;t quite right</li>
<li>Take what you get and make it your own</li>
<li>It&rsquo;s easy to fool yourself into thinking that the AI is doing a great job if you don&rsquo;t
have the expertise to evaluate the output. Be critical and test things out. If it&rsquo;s
medical advice, check with a professional.</li>
</ol>
<p>It&rsquo;s good to have a template now for creating custom exercise
programs. Next time I want to get back into shape or continue the existing
program, I can just tweak the prompt and have a new plan generated.</p>
<p>Speaking of templates&hellip;</p>
<h2 id="creating-your-own-custom-exercise-program">Creating your own custom exercise program</h2>
<p>If you want to create your own custom exercise program, here&rsquo;s this basic
template I used:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-text">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">You are an expert AI personal trainer. Please create a customized
</span></span><span class="line"><span class="cl">exercise plan for me.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Current fitness level: [Describe your recent activity, strength level, 
</span></span><span class="line"><span class="cl">and any relevant history]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Goals: [What do you want to achieve?]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Available equipment: [List everything you have access to]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Constraints: [Time limitations, experience level, injuries, etc.]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Request: Create a [duration] exercise plan that includes:
</span></span><span class="line"><span class="cl">- A daily workout schedule with specific exercises, sets, and reps
</span></span><span class="line"><span class="cl">- Detailed descriptions and/or video links for proper form
</span></span><span class="line"><span class="cl">- Progress tracking suggestions
</span></span><span class="line"><span class="cl">- Nutritional guidance to support my goals
</span></span><span class="line"><span class="cl">- Motivational tips
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Please format the schedule as a table showing what to do each day.</span></span></code></pre></div></div>
<p>Then, if you want a calendar file:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-text">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Create a Python script that generates an `.ics` file that I can import into my
</span></span><span class="line"><span class="cl">calendar. This will be easier to debug and modify.</span></span></code></pre></div></div>
<p>You can ask for the <code>.ics</code> file directly, but it may be more error-prone.</p>
<p>The nice thing about this approach is the customized nature of it. What we used
to get from fitness apps was generic plans based on broad categories of users.
Now, we can get something tailored to our individual needs. Not just picking
from a set of predefined workout templates. And since it is tailored to you,
and generated on demand, having the calendar file is also a big plus.</p>
<hr>
<p>Illustration by <a href="https://unsplash.com/@vectorelements?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">VectorElements</a> on <a href="https://unsplash.com/illustrations/man-practicing-yoga-tree-pose-in-living-room-vGDsMK9Wc1k?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></p>
]]></description></item><item><title>Building a Phaser 2D grid game with Claude</title><link>https://tracyatteberry.com/posts/infection/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/infection/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/infection/infection_top.png" referrerpolicy="no-referrer">
            </div><h1 id="building-a-phaser-2d-grid-game-with-claude">Building a Phaser 2D grid game with Claude</h1>
<h2 id="the-experiment">The experiment</h2>
<p>This project started with a simple question: How well could an AI coding
assistant help me build a game using a framework I&rsquo;d never touched before?</p>
<p>I&rsquo;ve spent years writing JavaScript - the language was familiar territory. I
had a passing acquaintance with game development concepts. But TypeScript? I
was a novice. Phaser? Absolutely zero experience. I expected Claude to excel
with TypeScript (plenty of training data), but suspected Phaser knowledge
would be sparse.</p>
<p>The central questions:</p>
<ul>
<li>Can Claude navigate a less-common API well enough to build something real?</li>
<li>Will I understand the generated code well enough to modify and extend it?</li>
<li>What does it take to give an AI assistant enough context to be genuinely
useful rather than just generating plausible-looking code that doesn&rsquo;t
work?</li>
</ul>
<p>I decided to find out. This is the story of building &ldquo;Infection: Germs vs
White Cells&rdquo; - a turn-based grid game where players compete to dominate the
board through strategic dot placement and chain reaction explosions. More
importantly, it&rsquo;s about what I learned collaborating with AI on unfamiliar
territory.</p>
<h2 id="the-foundation-getting-started">The foundation: Getting started</h2>
<p>I chose a tech stack that mixed familiar and new territory: Phaser 3 for the
game engine, Vue 3 for UI, TypeScript for type safety, and Vite for fast
development builds. Vue would handle menus and overlays while Phaser managed
gameplay.</p>
<p>The first challenge was connecting Vue and Phaser - they&rsquo;re designed for
different purposes and don&rsquo;t naturally integrate. After researching
examples, we settled on an EventBus pattern. PhaserGame.vue became the
bridge component that initializes the Phaser game and sets up bidirectional
communication through events.</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// Phaser scene emits events to Vue
</span></span></span><span class="line"><span class="cl"><span class="nx">EventBus</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="s1">&#39;current-scene-ready&#39;</span><span class="p">,</span> <span class="k">this</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">EventBus</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="s1">&#39;level-completed&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">winner</span><span class="o">:</span> <span class="s1">&#39;player&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Vue listens and responds
</span></span></span><span class="line"><span class="cl"><span class="nx">EventBus</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;level-completed&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Update UI, show victory screen, etc.
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>This pattern worked well throughout the project. Phaser handled game
logic, Vue handled UI chrome, and they stayed cleanly separated.</p>
<p>The initial gameplay came together surprisingly fast. Within the first
session, we had a working grid where players could click cells, place dots,
and see them explode when cells reached capacity. The turn system worked.
Player indicators updated correctly. The win condition detected when one
player controlled the entire board.</p>
<p>Claude was excellent at generating boilerplate and structure for familiar
patterns. The Vue-Phaser bridge? That exists in lots of projects. Basic game
loops? Common pattern. TypeScript interfaces for game state? Standard stuff.</p>
<h2 id="core-mechanics-making-it-feel-like-a-game">Core mechanics: Making it feel like a game</h2>
<p>Once the foundation worked, we focused on game feel. The core mechanic is
simple: click a cell to add a dot. When dots exceed a cell&rsquo;s capacity, the
cell explodes and distributes dots to adjacent cells. This can trigger chain
reactions that flip opponent cells to your color.</p>
<p>Cell capacity depends on position:</p>
<ul>
<li>Corner cells hold 2 dots (2 neighbors)</li>
<li>Edge cells hold 3 dots (3 neighbors)</li>
<li>Interior cells hold 4 dots (4 neighbors)</li>
<li>Blocked cells hold nothing and don&rsquo;t contribute to neighbor capacity</li>
</ul>
<p>The explosion logic needed timing. If chain reactions happened instantly,
players couldn&rsquo;t follow what was happening. We added a 300ms delay between
explosions, just enough to watch the cascade unfold without feeling sluggish.</p>
<p>Next we needed variety, different board sizes, obstacles that blocked certain
cells, and increasing difficulty. We needed levels. Rather than storing levels
in a database, we defined them in code as a linked list structure. Each level
points to the next, making navigation intuitive:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">currentLevel</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getCurrentLevel</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">nextLevel</span> <span class="o">=</span> <span class="nx">currentLevel</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">nextLevel</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">loadLevel</span><span class="p">(</span><span class="nx">nextLevel</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">handleGameOver</span><span class="p">(</span><span class="nx">winner</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>We also added undo functionality, because clicking the wrong cell feels
terrible in a strategy game. The game tracks the last 50 moves and can roll
back the board state. This feature can also be quite useful for testing and
debugging.</p>
<h2 id="making-it-polished-ui-and-ux">Making it polished: UI and UX</h2>
<p>A working game isn&rsquo;t the same as a game that feels good to play. We spent
time on visual polish - dots that pulse when placed, smooth animations,
satisfying sound effects for placement and chain reactions.</p>
<p>The Settings scene let players configure their experience: sound effects
on/off, player colors, which level set to play. The responsive design
ensured the grid centered properly on different screen sizes.</p>
<p>The tricky part was state preservation. When players navigated from the game
to settings and back, they expected their game to still be there. This
required careful management of Phaser&rsquo;s scene lifecycle - sleep and wake
rather than destroy and recreate. We&rsquo;ll come back to this because it caused
one of our biggest bugs.</p>
<h2 id="building-ai-opponents-from-random-to-strategic">Building AI opponents: From random to strategic</h2>
<p>A game against yourself gets boring quickly. We needed a computer opponent,
but I didn&rsquo;t want to build a perfect player - that&rsquo;s no fun either. Instead,
we implemented four difficulty levels with escalating sophistication.</p>
<p><strong>Easy AI</strong> picks random valid moves. No strategy, just valid placement.</p>
<p><strong>Medium AI</strong> looks for tactical opportunities:</p>
<ul>
<li>Explode fully loaded cells (capacity reached)</li>
<li>Claim corner and edge cells (harder for opponent to capture)</li>
<li>Otherwise pick randomly</li>
</ul>
<p><strong>Hard AI</strong> adds offensive tactics:</p>
<ul>
<li>Prioritize full cells adjacent to opponent cells (capture on explosion)</li>
<li>Explode full cells next to opponent&rsquo;s full cells (trigger counter-chains)</li>
<li>Fall back to medium strategy</li>
</ul>
<p><strong>Expert AI</strong> evaluates positional advantage using &ldquo;ullage&rdquo; (remaining
capacity). It seeks cells where adding a dot gives advantage over all
adjacent opponent cells, forcing the opponent into difficult positions.</p>
<p>Each level specifies its AI difficulty, so players face escalating challenge
as they progress through a level set.</p>
<p>The development approach: start simple, iterate to complexity. We built the
dumbest thing that could work (random moves), then made it smarter
incrementally based on playtesting feedback.</p>
<h2 id="architecture-evolution-when-code-gets-messy">Architecture evolution: When code gets messy</h2>
<p>Here&rsquo;s where I made my first major mistake: I put too much in Game.ts.
In self-defense, I didn&rsquo;t know yes what the architecture <em>should</em> look like yet.
So I deferred those decisions until later by putting everything in one place.</p>
<p>At first, this seemed fine. The game logic lived in the game scene. Makes
sense, right? But as features accumulated, Game.ts grew to over 1000 lines.
It handled grid creation, cell capacity calculations, explosion logic, AI
moves, UI updates, state persistence, settings management, and level
progression. Reading it required holding too many concepts in your head at
once.</p>
<p>The pain became obvious when bugs appeared. Tracking down a state persistence
bug meant wading through explosion logic and UI code. Fixing the play order
required understanding grid creation. Everything touched everything.</p>
<p>We needed a separation of concerns. Not for &ldquo;clean code&rdquo; aesthetics, but
because the cognitive complexity made changes risky and debugging slow.</p>
<p>The refactoring happened incrementally, driven by specific pain points:</p>
<p><strong>GameStateManager</strong> emerged when state bugs appeared. We needed one clear
place responsible for saving and loading game state to Phaser&rsquo;s registry,
handling undo history, and tracking level progression.</p>
<p><strong>GridManager</strong> split out when grid logic got complex. Cell capacity
calculations, blocked cell handling, hover states, and visual styling didn&rsquo;t
belong mixed with game logic.</p>
<p><strong>GameUIManager</strong> formed when UI updates scattered throughout the code. One
change to the player indicator required hunting through multiple methods.
Now UI creation and updates live in one place.</p>
<p><strong>SettingsManager</strong> centralized the synchronization between localStorage and
Phaser&rsquo;s registry. Settings read priority became explicit: registry first,
then localStorage, then defaults.</p>
<p><strong>BoardStateManager</strong> extracted the core game logic - explosion mechanics,
chain reactions, win condition detection. This became the pure game engine,
separate from Phaser rendering concerns.</p>
<p>Each refactoring happened when the pain became clear, not as a planned
&ldquo;rewrite day.&rdquo; We didn&rsquo;t wait for the perfect time to refactor. We
refactored when the current structure made the next feature difficult.</p>
<p>The linked list structure for levels proved elegant. Rather than tracking
level indices and bounds-checking arrays, levels just know their next level.
The code reads naturally: <code>if (currentLevel.isLast())</code> instead of <code>if (currentLevelIndex &gt;= levels.length - 1)</code>.</p>
<p>After refactoring, most manager classes stayed under 500 lines with single,
clear responsibilities. The Game scene itself still exceeds 800 lines (it
orchestrates all the managers and handles complex scene lifecycle), but the
cognitive load dropped dramatically. You can now understand GridManager
without knowing anything about state persistence or AI strategy.</p>
<h2 id="the-testing-awakening">The testing awakening</h2>
<p>I need to confess something: I allowed a lot of code to be written before writing
tests.</p>
<p>My rationale seemed sound at the time. I was learning Phaser&rsquo;s architecture
and didn&rsquo;t want to constantly rewrite tests as I figured out the right
patterns. Better to get something working first, then add tests once the
architecture stabilized.</p>
<p>This was expensive.</p>
<p>Without tests, every refactoring risked breaking something. I&rsquo;d extract
GameStateManager and then manually click through the entire game to verify
level progression still worked. I&rsquo;d modify explosion logic and hand-test
edge cases by setting up specific board states. Bugs appeared, got fixed,
then reappeared weeks later because nothing prevented regression.</p>
<p>The wake-up call came during a refactoring that broke the undo system in a
subtle way. The game worked for new games, but loading a saved game with
undo history crashed. I&rsquo;d fixed this bug before. Now it was back.</p>
<p>We needed comprehensive test coverage.</p>
<p>Working with Claude, we built a four-phase testing plan:</p>
<p><strong>Phase 1: Core data structures</strong> (69 tests)</p>
<ul>
<li>Level class: linked list navigation, property access, last level detection</li>
<li>LevelSet class: level management, traversal, bounds checking</li>
</ul>
<p><strong>Phase 2: Manager classes</strong> (196 tests)</p>
<ul>
<li>SettingsManager: localStorage/registry sync, defaults, read priority</li>
<li>GameStateManager: save/load, undo/redo, move history limits</li>
<li>LevelSetManager: loading definitions, level set switching</li>
<li>BoardStateManager: game logic, explosions, win conditions</li>
</ul>
<p><strong>Phase 3: Game logic</strong> (88 tests)</p>
<ul>
<li>GridManager: cell capacity, blocked cells, hover states</li>
<li>ComputerPlayer: all four difficulty levels, move validation</li>
</ul>
<p><strong>Phase 4: UI layer</strong> (49 tests)</p>
<ul>
<li>GameUIManager: element creation, updates, positioning</li>
</ul>
<p>We used Vitest because it&rsquo;s fast, has excellent TypeScript support, and
provides a clean testing API. Tests lived next to their source files:
<code>GridManager.ts</code> and <code>GridManager.test.ts</code> in the same directory.</p>
<p>The test suite currently has <strong>514 tests across 20 test files</strong>, and they
run in about 1.2 seconds. Fast enough to run on every save during
development.</p>
<p>Writing tests after the fact taught me something: test-driven development
exists for good reasons. The tests we wrote exposed edge cases we&rsquo;d never
considered. They caught bugs that would have appeared weeks later. They made
refactoring safe instead of terrifying.</p>
<p>If I started this project over, I&rsquo;d write tests earlier. Not because
tests are &ldquo;best practice,&rdquo; but because they would have saved me days of
debugging time.</p>
<h2 id="-key-challenges-and-debugging-victories">🪲 Key challenges and debugging victories</h2>
<p><em>The real learning happened when things broke. Here are four bugs that
taught me the most about Phaser, systematic debugging, and AI collaboration.</em></p>
<h3 id="challenge-1-the-settings-scene-reset-bug">Challenge 1: The settings scene reset bug</h3>
<p><strong>Symptom:</strong> Navigate to Settings, change nothing, click Back. The game
resets to the first level. Your in-progress game vanishes.</p>
<p><strong>First instinct:</strong> The state isn&rsquo;t being saved. We added logging to
GameStateManager. The state was saving perfectly. The state was loading
correctly too. What?</p>
<p><strong>Root cause:</strong> We were using <code>scene.start()</code> to transition between Game and
Settings scenes. This method destroys the current scene and creates a fresh
instance of the target scene. When returning to Game, we got a brand new
Game scene that ran its <code>create()</code> method, which loaded the first level by
default.</p>
<p><strong>The fix:</strong> Phaser scenes have a lifecycle: <code>create()</code> runs once when the
scene is first instantiated. <code>wake()</code> runs when a sleeping scene becomes
active again. We needed:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// In Game scene
</span></span></span><span class="line"><span class="cl"><span class="nx">navigateToSettings() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">sleep</span><span class="p">();</span>  <span class="c1">// Not scene.start()
</span></span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">launch</span><span class="p">(</span><span class="s1">&#39;Settings&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// In Settings scene
</span></span></span><span class="line"><span class="cl"><span class="nx">goBack() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">stop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">wake</span><span class="p">(</span><span class="s1">&#39;Game&#39;</span><span class="p">);</span>  <span class="c1">// Wake the sleeping scene
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>We also added a <code>settingsDirty</code> flag. If settings actually changed, the Game
scene&rsquo;s <code>wake()</code> method reloads them. Otherwise, it just resumes.</p>
<p><strong>Lesson:</strong> Understanding framework lifecycles matters. Claude knew the
general pattern but didn&rsquo;t initially suggest wake/sleep because the Phaser
API wasn&rsquo;t in its training data as heavily. Providing links to current
Phaser 3.90 documentation helped tremendously. Without docs, Claude would
continue to guess based on older API versions, wasting time.</p>
<h3 id="challenge-2-level-progression-bug">Challenge 2: Level progression bug</h3>
<p><strong>Symptom:</strong> Complete the first level, click &ldquo;Next Level.&rdquo; You see the first
level again instead of level 2.</p>
<p><strong>The investigation:</strong> We added logging to track what level was being loaded:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[GameStateManager] Saved state:
</span></span></span><span class="line"><span class="cl"><span class="sb">  </span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">boardState</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[BoardStateManager] Setting state:
</span></span></span><span class="line"><span class="cl"><span class="sb">  </span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">boardState</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span></span></span></code></pre></div></div>
<p>The logs revealed the issue: we were saving an empty <code>boardState</code> to the
registry, which triggered the &ldquo;new game&rdquo; code path that loaded level 1.</p>
<p><strong>Root cause:</strong> The level completion logic set a <code>loadNextLevel</code> flag, but the
state persistence logic also saw an empty board and saved it. This was a race
condition where both actions happened simultaneously, and the empty state won.</p>
<p><strong>The fix:</strong> Prioritize the <code>loadNextLevel</code> flag. Check it before looking at
board state:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">wake() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">savedState</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">stateManager</span><span class="p">.</span><span class="nx">loadFromRegistry</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">savedState</span><span class="o">?</span><span class="p">.</span><span class="nx">loadNextLevel</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">nextLevel</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">currentLevel</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">nextLevel</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">this</span><span class="p">.</span><span class="nx">loadLevel</span><span class="p">(</span><span class="nx">nextLevel</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span>  <span class="c1">// Exit early
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Otherwise restore board state
</span></span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">savedState</span><span class="o">?</span><span class="p">.</span><span class="nx">boardState</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">restoreBoardState</span><span class="p">(</span><span class="nx">savedState</span><span class="p">.</span><span class="nx">boardState</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><strong>Lesson:</strong> The linked list structure actually helped here. The code <code>if (nextLevel)</code> makes it obvious we&rsquo;re checking if a next level exists. With
array indices, we&rsquo;d have <code>if (currentLevelIndex + 1 &lt; levels.length)</code>, which
is more error-prone.</p>
<h3 id="challenge-3-level-set-changes-not-taking-effect">Challenge 3: Level set changes not taking effect</h3>
<p><strong>Symptom:</strong> User selects a different level set in Settings, clicks &ldquo;Play
Game.&rdquo; They see the old level set instead.</p>
<p><strong>Root cause:</strong> The Game scene wasn&rsquo;t checking if settings changed while it
was sleeping. It would wake up and continue with the old LevelSetManager.</p>
<p><strong>The fix:</strong> The <code>settingsDirty</code> flag from Challenge 1 solved this too. When
settings change, the flag gets set. On wake, if the flag is set, reload all
settings:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">wake() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">settingsDirty</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">game</span><span class="p">.</span><span class="nx">registry</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s1">&#39;settingsDirty&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">settingsDirty</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">reloadAllSettings</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">game</span><span class="p">.</span><span class="nx">registry</span><span class="p">.</span><span class="kr">set</span><span class="p">(</span><span class="s1">&#39;settingsDirty&#39;</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// ... rest of wake logic
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>We also needed defensive logic. What if the user changed both player color
AND level set? Both changes needed to take effect together, not sequentially
with potential state corruption between them.</p>
<p><strong>Lesson:</strong> State synchronization across scene transitions requires explicit
change detection. Don&rsquo;t assume data hasn&rsquo;t changed while your scene slept.</p>
<h3 id="challenge-4-memory-leaks-from-event-listeners">Challenge 4: Memory leaks from event listeners</h3>
<p><strong>Symptom:</strong> During manual testing, I noticed the browser memory footprint
growing as I transitioned between scenes repeatedly. Something was leaking.</p>
<p><strong>The investigation:</strong> We added event listener counting to each scene:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">shutdown() {</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] Listeners before cleanup:
</span></span></span><span class="line"><span class="cl"><span class="sb">    </span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">listenerCount</span><span class="p">(</span><span class="s1">&#39;pointerdown&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">cleanupEventListeners</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] Listeners after cleanup:
</span></span></span><span class="line"><span class="cl"><span class="sb">    </span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">listenerCount</span><span class="p">(</span><span class="s1">&#39;pointerdown&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The counts kept growing. Event listeners weren&rsquo;t being cleaned up on scene
transitions.</p>
<p><strong>The fix:</strong> We added explicit cleanup methods to every scene using TDD.
First, write a test that verifies listeners are removed:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">it</span><span class="p">(</span><span class="s1">&#39;should clean up all button event listeners on shutdown&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">scene</span><span class="p">.</span><span class="nx">create</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">beforeCount</span> <span class="o">=</span> <span class="nx">scene</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">listenerCount</span><span class="p">(</span><span class="s1">&#39;pointerdown&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">beforeCount</span><span class="p">).</span><span class="nx">toBeGreaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">scene</span><span class="p">.</span><span class="nx">cleanupButtonListeners</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">afterCount</span> <span class="o">=</span> <span class="nx">scene</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">listenerCount</span><span class="p">(</span><span class="s1">&#39;pointerdown&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">afterCount</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>Then implement cleanup:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">shutdown() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">cleanupButtonListeners</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">cleanupGridListeners</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">cleanupUIListeners</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>We built a live testing dashboard (<code>npm run test:live</code>) that shows real-time
event listener counts during rapid scene transitions. You can watch the
numbers and verify they don&rsquo;t accumulate.</p>
<p><em>[Screenshot: Live testing dashboard showing event listener metrics would go
here]</em></p>
<p><strong>Lesson:</strong> Building testing infrastructure surfaced issues we didn&rsquo;t know
existed. The process of creating the dashboard forced us to think about how
to measure cleanup, which led us to Phaser&rsquo;s <code>listenerCount()</code> API and
revealed leaks throughout the codebase. We now have 95% test automation for
event cleanup validation and zero known memory leaks.</p>
<p><strong>Common thread:</strong> Each bug revealed itself through evidence gathering
(logging, instrumentation) rather than guessing. This pattern became the
foundation for our systematic debugging approach.</p>
<h2 id="the-debugging-discipline">The debugging discipline</h2>
<p>These four challenges revealed something important: guessing doesn&rsquo;t scale,
even for AI.</p>
<p>Early in the project, Claude would hit a bug and immediately suggest a fix.
Didn&rsquo;t work? Try another. Still broken? Try a third. This guess-and-check
thrashing wasted hours and often made problems worse.</p>
<p>I had access to Jesse Vincent&rsquo;s systematic debugging &ldquo;superpowers&rdquo; (think of
them as process discipline plugins for Claude). They just weren&rsquo;t being
enforced. After enough frustration, I made the systematic debugging
superpower mandatory in CLAUDE.md - the project documentation file that
guides Claude&rsquo;s behavior. The protocol is straightforward:</p>
<ol>
<li><strong>Gather evidence first</strong> - Add logging to understand what&rsquo;s actually
happening, not what you think is happening</li>
<li><strong>Analyze patterns</strong> - Compare working vs broken implementations</li>
<li><strong>Test single hypotheses</strong> - Make one targeted change to test a theory</li>
<li><strong>Fix root causes</strong> - Address the actual problem, not symptoms</li>
</ol>
<p>It also includes this note: &ldquo;Systematic debugging is 5x faster than
guess-and-check thrashing.&rdquo;</p>
<p>Before adding this additional directive, Claude would often suggest fixes immediately:
&ldquo;Try changing this API call&rdquo; or &ldquo;Maybe add this flag.&rdquo; After adding it,
Claude would first suggest adding instrumentation: &ldquo;Let&rsquo;s add logging to see
what values we&rsquo;re actually getting.&rdquo;</p>
<p>The difference was dramatic. Bugs that previously took hours to solve took
30 minutes. We stopped creating bugs while fixing bugs.</p>
<p>Full disclosure: I&rsquo;m a senior developer and I knew better. But curiosity got
the best of me. One of my goals was understanding AI behavior patterns to
collaborate more effectively on future projects. I wanted to see if Claude
could guess its way to solutions.</p>
<p>The answer: Sometimes, but unreliably. With sufficient context, guessing
(or pattern recognition that looks like guessing) often worked. On
unfamiliar frameworks like Phaser, it usually failed.</p>
<p><strong>Key insights:</strong></p>
<ul>
<li>For Claude: Evidence before action. Always.</li>
<li>For me: Enforce systematic approaches through CLAUDE.md, don&rsquo;t rely on AI
self-discipline.</li>
</ul>
<p>For scene lifecycle bugs, we added comprehensive logging:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-typescript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">create() {</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] ===== SCENE CREATE START =====`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ... scene creation code ...
</span></span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] ===== SCENE CREATE END =====`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">wake() {</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] ===== SCENE WAKE START =====`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">settings</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">game</span><span class="p">.</span><span class="nx">registry</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s1">&#39;settingsDirty&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">] Settings dirty: </span><span class="si">${</span><span class="nx">settings</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ... wake logic ...
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>This logging made scene transitions visible. We could see exactly when
<code>create()</code> ran vs <code>wake()</code>, what data each method received, and what order
operations occurred in. Bugs became obvious instead of mysterious.</p>
<h2 id="working-with-claude-what-worked-what-didnt">Working with Claude: What worked, what didn&rsquo;t</h2>
<p><em>Hiring managers care about productivity and code quality. Here&rsquo;s an honest
assessment of where AI helped, where it struggled, and what that means for
development teams.</em></p>
<h3 id="what-worked-well">What worked well</h3>
<p><strong>Boilerplate and structure:</strong> Claude excels at generating TypeScript
interfaces, class structures, and common patterns. Need a manager class with
standard CRUD operations? Claude writes it in seconds.</p>
<p><strong>Pattern recognition:</strong> &ldquo;This looks like the command pattern&rdquo; or &ldquo;This is
similar to the observer pattern we used for events&rdquo; - Claude connects new
problems to solved problems effectively.</p>
<p><strong>Architectural improvements:</strong> When I recognized Game.ts had grown too
large at 1000+ lines, Claude suggested extraction patterns that made sense.
The refactoring strategies were sound once the problem was identified.</p>
<p><strong>Test writing:</strong> Once we established a pattern for one test file, Claude
could generate similar tests for other classes. The 514 tests would have
taken weeks to write manually.</p>
<p><strong>Systematic debugging:</strong> Once convinced to use the systematic debugging superpower,
Claude followed it reliably. This helped enormously and saved a ton of time.</p>
<h3 id="what-required-guidance">What required guidance</h3>
<p><strong>Phaser API specifics:</strong> This was the biggest challenge. Claude&rsquo;s training
data apparently has much less Phaser content than TypeScript or Vue. It
would suggest API calls that sounded plausible but didn&rsquo;t exist, or use
patterns from older Phaser versions.</p>
<p>The solution: Provide links to current Phaser 3.90 documentation. When I
sent Claude snippets from official docs, suggestions became more accurate.
Without docs, Claude would guess, and guessing wasted time.</p>
<p><strong>Project-specific architecture decisions:</strong> Claude couldn&rsquo;t decide whether
to use localStorage vs Phaser&rsquo;s registry, or when to extract a manager class
vs keep code together. These decisions required human judgment based on
project context. Clearer instructions in CLAUDE.md helped, but some decisions
still needed human guidance.</p>
<p><strong>Refactoring timing:</strong> Claude would sometimes suggest refactoring when we
needed to ship, or suggest shipping when the code really needed cleanup. The
&ldquo;when&rdquo; required human intuition.</p>
<p><strong>Testing discipline:</strong> Without explicit guidance, Claude would tend to
neglect testing. It would happily write feature after feature without
suggesting tests. The comprehensive test suite only happened because I
explicitly requested it and then added requirements to CLAUDE.md.</p>
<p><strong>Claiming victory too early:</strong> Claude would repeatedly declare a bug fixed
after a potential fix. Before any verification it was ready to mark the item
as complete and move on. It needed reminders to verify that changes actually worked.</p>
<h3 id="the-claudemd-evolution">The CLAUDE.md evolution</h3>
<p>CLAUDE.md started as a basic README: project structure, how to run tests,
basic architecture notes.</p>
<p>It evolved into the project brain - a comprehensive guide that overrides
Claude&rsquo;s default behavior:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-markdown">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## 🚨 MANDATORY DEBUGGING PROTOCOL
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**FOR ANY TECHNICAL ISSUE - ALWAYS use systematic debugging**</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**FORBIDDEN PATTERNS (cause more bugs than they fix):**</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;Quick fixes&#34; and guesswork - <span class="gs">**STRICTLY PROHIBITED**</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> Trying random API calls without understanding root cause
</span></span><span class="line"><span class="cl">- Making multiple changes at once</span></span></code></pre></div></div>
<p>Before this protocol existed, Claude would get caught in guessing loops. Try
a fix, doesn&rsquo;t work, try another, still broken, try a third. The mandatory
protocol broke this pattern.</p>
<p>We documented every resolved bug: symptoms, root causes, fixes. When similar
issues appeared later, the documentation provided patterns to recognize.</p>
<p>The key realization: Documentation is bidirectional. I taught Claude about
the project, and the process of explaining things to Claude clarified my own
understanding. Writing clear instructions forced me to think clearly about
solutions. Some of the lessons learned here will be added to the global
<code>~/.claude/CLAUDE.md</code> file for future projects.</p>
<p><strong>Bottom line:</strong> AI assistance works best as a partnership. The human brings
judgment, context, and architectural vision. The AI brings speed,
consistency, and tireless execution of well-defined tasks. Neither replaces
the other.</p>
<h2 id="lessons-learned">Lessons learned</h2>
<p><em>Here&rsquo;s what I&rsquo;d tell my past self, or anyone starting a similar project.</em></p>
<h3 id="technical-lessons">Technical lessons</h3>
<p><strong>1. Test early, test often</strong></p>
<p>Writing tests after code cost significant rework time. Tests exposed edge
cases we&rsquo;d never considered. They caught regressions before they shipped.
They made refactoring safe.</p>
<p>If I restarted this project, tests would come first. Not as &ldquo;best practice&rdquo;
dogma, but as a practical time-saving tool.</p>
<p><strong>2. Scene lifecycle matters</strong></p>
<p>Understanding <code>create()</code> vs <code>wake()</code> vs <code>sleep()</code> vs <code>shutdown()</code> is
critical in Phaser. The wrong method causes subtle bugs. The right method
makes everything work.</p>
<p><strong>3. Registry over localStorage</strong></p>
<p>We used Phaser&rsquo;s registry as the single source of truth for runtime state,
with localStorage only for persistent settings. This prevented
synchronization bugs between storage systems.</p>
<p><strong>4. Separation of concerns reduces cognitive load</strong></p>
<p>This isn&rsquo;t about &ldquo;clean code&rdquo; aesthetics. When Game.ts exceeded 1000 lines,
making changes became risky because every change could affect multiple
unrelated features. After extracting manager classes, each file became
understandable in isolation.</p>
<p><strong>5. Linked lists for sequential navigation</strong></p>
<p>The linked list structure for levels made code readable: <code>currentLevel. next()</code> instead of array index math. It also made the progression concept
explicit in the data structure.</p>
<p><strong>6. Event cleanup is not optional</strong></p>
<p>Memory leaks accumulate silently. Without explicit cleanup and testing, the
browser&rsquo;s memory footprint grows. Players might not notice on first play,
but the leak still exists.</p>
<h3 id="ai-collaboration-lessons">AI collaboration lessons</h3>
<p><strong>1. Documentation is bidirectional</strong></p>
<p>Teaching Claude about the project clarified my own thinking. Writing
instructions forced clear problem statements. The CLAUDE.md file became as
valuable for me as for the AI.</p>
<p><strong>2. Systematic approaches scale</strong></p>
<p>Ad-hoc debugging doesn&rsquo;t work on complex projects. The mandatory debugging
protocol saved enormous amounts of time by preventing guess-and-check
thrashing.</p>
<p><strong>3. Start simple, iterate to complexity</strong></p>
<p>We didn&rsquo;t architect everything perfectly from day one. We built the
simplest thing that could work (random AI, basic grid) then made it more
sophisticated incrementally. This approach worked much better than trying to
design the perfect system upfront.</p>
<p><strong>4. Context files matter</strong></p>
<p>CLAUDE.md became the project brain. It captured architectural decisions,
debugging patterns, resolved bugs, and mandatory workflows. Without it,
every conversation started from zero.</p>
<p><strong>5. AI is better with constraints</strong></p>
<p>Claude works best with clear protocols and explicit constraints. &ldquo;Debug this
bug&rdquo; leads to guessing. &ldquo;Follow the systematic debugging protocol to
investigate this bug&rdquo; leads to instrumentation and evidence gathering.</p>
<h3 id="process-lessons">Process lessons</h3>
<p><strong>1. Git commit messages tell the story</strong></p>
<p>Using conventional commits (feat:, fix:, refactor:, test:) made history
searchable. When debugging the level progression bug, searching for &ldquo;fix:
level&rdquo; immediately found commit <code>0eb8769</code>. When preparing this blog post,
running <code>git log --oneline --reverse</code> showed the project evolution clearly.</p>
<p><strong>2. Refactoring is continuous</strong></p>
<p>We didn&rsquo;t schedule &ldquo;refactoring week.&rdquo; We refactored when current structure
made the next feature difficult:</p>
<ul>
<li>Game.ts hit 1000+ lines → extracted GameStateManager</li>
<li>State bugs appeared → extracted BoardStateManager</li>
<li>Grid logic got complex → extracted GridManager</li>
<li>UI updates scattered → extracted GameUIManager</li>
</ul>
<p>Each refactoring addressed immediate pain, not theoretical future problems.</p>
<p><strong>3. Build testing infrastructure</strong></p>
<p>The live testing dashboard (<code>npm run test:live</code>) seemed like overkill for a
simple game. But building it forced us to think about how to measure event
cleanup, which revealed the <code>listenerCount()</code> API, which exposed leaks we
didn&rsquo;t know existed. The infrastructure paid for itself.</p>
<p><strong>4. Document gotchas immediately</strong></p>
<p>Every resolved bug went into CLAUDE.md immediately. The documentation
prevented the same bug from reappearing and provided patterns for similar
issues. Future me thanked past me repeatedly.</p>
<h2 id="the-final-numbers">The final numbers</h2>
<p><em>What AI-assisted development produced:</em></p>
<p><strong>Code quality metrics:</strong></p>
<ul>
<li><strong>514 tests</strong> across 20 test files (started with 0, grew through 4-phase
testing plan)</li>
<li><strong>95% test automation</strong> for event cleanup validation</li>
<li><strong>~1.2 second</strong> test suite execution time (fast enough to run on every
save)</li>
<li><strong>Zero known memory leaks</strong> after systematic cleanup and monitoring</li>
</ul>
<p><strong>Architecture:</strong></p>
<ul>
<li><strong>9 core manager classes</strong> handling distinct concerns (extracted from
monolithic Game.ts)</li>
<li><strong>10 Phaser scenes</strong> managing game flow (Boot → Preloader → Splash →
MainMenu → Game/About/Tutorial/Settings → LevelOver → GameOver)</li>
<li><strong>4 AI difficulty levels</strong> implementing escalating strategic sophistication</li>
</ul>
<p><strong>Content:</strong></p>
<ul>
<li><strong>Multiple level sets</strong> with 5-7 levels each</li>
<li><strong>Variable board sizes</strong> and blocked cell patterns for strategic variety</li>
<li><strong>100+ commits</strong> documenting the evolution with conventional commit format</li>
</ul>
<p>The game is playable, maintainable, and well-tested. More importantly, the
codebase is understandable. A developer new to the project could read
GridManager without knowing anything about AI strategy, or modify explosion
logic without understanding state persistence. That&rsquo;s what separation of
concerns actually buys you.</p>
<h2 id="was-it-worth-it">Was it worth it?</h2>
<p>Absolutely.</p>
<p>I built a working game using a framework I&rsquo;d never used, and the code is
maintainable enough that I&rsquo;d be comfortable handing it to another developer.
That&rsquo;s the real test.</p>
<p>Claude bridged knowledge gaps effectively where it had training data
(TypeScript, design patterns). Where it lacked context (Phaser specifics,
project architecture), providing documentation and clear constraints made it
productive.</p>
<p>The discipline of testing and documentation paid dividends. The 514-test
suite catches regressions before they reach production. The CLAUDE.md file
captures institutional knowledge that would otherwise live only in my head.
The systematic debugging protocol prevents guess-and-check thrashing that
wastes hours.</p>
<p><strong>Key insight:</strong> AI works best with clear constraints and feedback. Without
the debugging protocol, Claude would guess. With it, Claude would gather
evidence. Without test requirements, Claude would skip tests. With
requirements, it wrote comprehensive coverage.</p>
<p>The game is playable and reasonably fun. The AI provides genuine challenge.
The animations feel responsive. Is the architecture perfect? Honestly, I
don&rsquo;t know. But it&rsquo;s good enough to ship, iterate on, and extend - which is
the point.</p>
<p>Would I do it again? Absolutely, but I&rsquo;d establish testing discipline
earlier and encode systematic approaches in CLAUDE.md from day one.</p>
<h2 id="try-it-yourself">Try it yourself</h2>
<p>Want to see the results or dig into the implementation?</p>
<ul>
<li><strong><a href="https://magicbydesign.com/infection" target="_blank" rel="noopener noreffer ">Play the game</a></strong> - Try it in your
browser (no installation required)</li>
<li><strong><a href="https://github.com/grymoire7/infection" target="_blank" rel="noopener noreffer ">View the source</a></strong> - Explore
the code with full commit history</li>
<li><strong><a href="https://github.com/grymoire7/infection/blob/main/CLAUDE.md" target="_blank" rel="noopener noreffer ">Read CLAUDE.md</a></strong>
<ul>
<li>See the &ldquo;project brain&rdquo; that guided development decisions</li>
</ul>
</li>
</ul>
<p>If you&rsquo;re considering AI-assisted development, especially with unfamiliar
frameworks, here&rsquo;s what I learned works:</p>
<ul>
<li>Provide current documentation when the AI lacks training data (links to
official docs beat guessing every time)</li>
<li>Establish systematic approaches early (debugging protocols, testing
requirements)</li>
<li>Write tests as you go, not retrospectively</li>
<li>Use a context file (CLAUDE.md) to capture architecture decisions and
patterns</li>
<li>Expect to teach the AI your project specifics - documentation is
bidirectional</li>
</ul>
<p><strong>The partnership model:</strong> You bring judgment, architectural vision, and
domain knowledge. AI brings speed, consistency, and tireless execution of
well-defined tasks. Neither replaces the other, but together they can tackle
unfamiliar territory effectively.</p>
<p>Now go build something.</p>
]]></description></item><item><title>Infection - A Phaser 2D grid game</title><link>https://tracyatteberry.com/portfolio/infection/</link><pubDate>Mon, 10 Nov 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/infection/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/infection/top.png" referrerpolicy="no-referrer">
            </div><p>Building a simple 2D grid-based game using Phaser and Claude AI.</p>
]]></description></item><item><title>Building a daily storytelling puzzle game</title><link>https://tracyatteberry.com/posts/stormoji/</link><pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/stormoji/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/stormoji/Stormoji_top.png" referrerpolicy="no-referrer">
            </div><h1 id="building-a-daily-privacy-first-creative-storytelling-puzzle-game">Building a daily privacy-first creative storytelling puzzle game</h1>
<p>When I set out to create Stormoji, I wanted to combine the daily engagement
of Wordle with the creative freedom of storytelling. The result is a
single-page web application where players see four random emojis each day
and weave them into imaginative narratives. What started as a simple idea
evolved into a technical exploration of deterministic algorithms,
privacy-first architecture, and the challenges of creating consistent
experiences without a backend.</p>
<figure style="margin-top: 15px">
  
  <figcaption>Main Stormoji screen</figcaption>
</figure>
<h2 id="the-concept-daily-creativity-with-shared-challenges">The concept: daily creativity with shared challenges</h2>
<p>Like Wordle, everyone sees the same emojis each day. But unlike Wordle&rsquo;s
single correct answer, every story is unique. The app presents four emojis
selected from eight categories: smileys, people, animals, food, activities,
objects, symbols, and travel. It challenges players to incorporate them all
into a coherent narrative.</p>
<p>The privacy-first approach was fundamental from the start. No accounts, no
servers, no data collection. Everything happens locally in the browser,
with stories automatically saved to localStorage for up to six months. This
creates an intimate creative space where users can express themselves
without worries about their stories being stored or analyzed.</p>
<h2 id="the-core-technical-challenge-deterministic-randomness">The core technical challenge: deterministic randomness</h2>
<p>The biggest technical hurdle was ensuring that every user sees the same
four emojis each day, regardless of when they visit or what device they use.
With no backend to coordinate this, the solution had to be entirely
client-side.</p>
<p>I implemented a deterministic random selection algorithm using date-based
seeding. The process works like this:</p>
<ol>
<li><strong>Date-based seed generation</strong>: Convert today&rsquo;s date to a <code>YYYY-MM-DD</code>
format string, which becomes the seed</li>
<li><strong>Seeded random function</strong>: Use a sine-based pseudo-random number
generator that produces identical results for the same seed</li>
<li><strong>Category selection</strong>: Randomly select four different emoji categories
using the seeded random function</li>
<li><strong>Emoji selection</strong>: Pick one emoji from each selected category</li>
<li><strong>Deterministic shuffle</strong>: Use a Fisher-Yates shuffle with the same seed
to randomize the final order</li>
</ol>
<p>This ensures that on any given day, all users receive identical emoji
combinations, creating that shared daily experience that gives these games
their social nature.</p>
<figure style="margin-top: 15px">
  
  <figcaption>Stormoji history</figcaption>
</figure>
<h2 id="building-the-csv-export-feature">Building the CSV export feature</h2>
<p>As users started accumulating stories, they wanted a way to preserve their
stories in case they lost their phones, changed computers, or switched
browsers. This led to implementing a CSV export feature, which proved slightly
more complex than initially expected.</p>
<p>The main challenges were:</p>
<p><strong>Proper CSV escaping</strong>: Stories can contain commas, quotes, and
newlines—all characters that break CSV formatting. I created an escape
function that wraps fields containing special characters in double quotes and
escapes internal quotes by doubling them.</p>
<p><strong>Browser compatibility</strong>: The export uses the Blob API to generate the file
and a temporary anchor element to trigger the download. This works in all
modern browsers but requires careful cleanup to prevent memory leaks.</p>
<p><strong>User experience</strong>: The export needed to handle edge cases gracefully. If
a user has no stories, they see a notification instead of triggering an
empty download. Success and error states are clearly communicated through
toast notifications.</p>
<p>I also redesigned the header menu, replacing the settings gear icon with a
hamburger menu that provides access to both the About dialog and the new
Export History option. The dropdown includes click-outside detection and
responsive positioning for mobile devices.</p>
<figure style="margin-top: 15px">
  
  <figcaption>Stormoji hamburger menu</figcaption>
</figure>
<h2 id="lessons-learned">Lessons learned</h2>
<p>Building Stormoji reinforced several valuable lessons about modern web
development:</p>
<p><strong>Deterministic algorithms are powerful</strong>: Using date-based seeding solved
the shared daily experience problem without requiring any server-side
coordination. The same approach could be applied to other daily challenge
applications. In fact, I later reused this basic algorithm for another project,
<a href="https://slightlymoreawesome.com" target="_blank" rel="noopener noreffer ">SlightlyMoreAwesome</a>.</p>
<p><strong>Privacy-first doesn&rsquo;t mean feature-poor</strong>: Despite being entirely
client-side, the app includes story persistence, export functionality, and
a rich user experience.</p>
<p><strong>CSV is deceptively complex</strong>: What seems like a simple format has many
edge cases. Proper escaping of special characters is crucial for generating
files that work reliably in spreadsheet applications.</p>
<p><strong>Vanilla JavaScript is still viable</strong>: The entire application runs without
frameworks, build tools, or dependencies. This results in instant loading,
zero overhead, and maximum compatibility. Sometimes the simplest solution
is the best one.</p>
<p>Throughout development, I tried to maintain a clean architecture that keeps the
code maintainable while delivering all the features that were planned.</p>
<h2 id="the-architecture-simplicity-by-design">The architecture: simplicity by design</h2>
<p>The application follows a clean separation of concerns:</p>
<p><strong>Data layer</strong>: localStorage with JSON serialization, handling story storage
and automatic cleanup of entries older than six months</p>
<p><strong>Business logic</strong>: Deterministic emoji selection, story management, and
export functionality</p>
<p><strong>Presentation layer</strong>: Responsive CSS with mobile-first design, semantic
HTML, and accessible interactions</p>
<p><strong>Event handling</strong>: Modular JavaScript with proper cleanup to prevent
memory leaks</p>
<p>This architecture makes the code maintainable and easy to understand,
while still providing features users expect from a modern web application.</p>
<h2 id="looking-forward">Looking forward</h2>
<p>Stormoji demonstrates that sophisticated web applications don&rsquo;t always need
complex infrastructures. Yet every implementation detail was an opportunity to
learn or reinforce some detail of web development. Still, there remain
several avenues for future improvement and exploration:</p>
<ul>
<li><strong>History import</strong>: Allow users to import previously exported CSV files to restore
their story history across devices</li>
<li><strong>Opt-in access to a read-only backend</strong>: For users who want to see an AI generated
story based on the day&rsquo;s emojis without storing any personal data. It could be
annotated with information on how the story references each emoji. Think of it
as a kind of &ldquo;hint&rdquo; system. The output could be cached an/d/or pre-coumputed.</li>
<li><strong>Dark mdoe</strong>: Because all apps are better with dark mode&hellip; right?</li>
</ul>
<p>If you&rsquo;re interested in trying it out, visit <a href="https://stormoji.com" target="_blank" rel="noopener noreffer ">stormoji.com</a>
to see today&rsquo;s emoji challenge. Or dive into the <a href="https://github.com/grymoire7/stormoji" target="_blank" rel="noopener noreffer ">source code</a>
to see how all these pieces come together in a single, dependency-free
deliverable.</p>
]]></description></item><item><title>Hyrum - A CLI gem for Hyrum's law</title><link>https://tracyatteberry.com/portfolio/hyrum/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/hyrum/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/hyrum/hyrum_top.png" referrerpolicy="no-referrer">
            </div><p>Building a gem.</p>
]]></description></item><item><title>You don't need GraphQL</title><link>https://tracyatteberry.com/posts/no_graphql/</link><pubDate>Tue, 07 Jan 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/no_graphql/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/no_graphql/graphql.png" referrerpolicy="no-referrer">
            </div><h2 id="introduction">Introduction</h2>
<p>If you&rsquo;ve built Rails applications in the last few years, you&rsquo;ve probably felt
the pressure to adopt GraphQL. The sales pitch is compelling: a more flexible
API, happier frontend developers, and an end to the dreaded over-fetching of
data. Companies like GitHub and Shopify have embraced it, and the ecosystem of
tools keeps growing. It&rsquo;s easy to feel like you&rsquo;re falling behind if you
haven&rsquo;t jumped on the GraphQL bandwagon.</p>
<p>But here&rsquo;s the thing: while GraphQL can be a powerful tool for certain
applications, it&rsquo;s become a default choice without enough critical examination
of its trade-offs. After spending several years building and maintaining Rails
applications both with and without GraphQL, I&rsquo;ve found that its carrying costs
often outweigh its benefits for typical Rails applications.</p>
<p>This isn&rsquo;t just about the initial learning curve or setup time. It&rsquo;s about the
ongoing maintenance burden, the hidden complexity costs, and the
often-overlooked strengths of Rails&rsquo; conventional approach to API design. In
many cases, the problems that GraphQL promises to solve can be addressed more
simply using Rails&rsquo; built-in tools and well-established patterns.</p>
<p>Let&rsquo;s explore why GraphQL might be more complexity than your Rails application
needs, and how to make this evaluation for your own projects.</p>
<h2 id="why-graphql-seems-attractive">Why GraphQL seems attractive</h2>
<p>Let&rsquo;s be honest: GraphQL&rsquo;s appeal is far from superficial. When you first see a
well-designed GraphQL API in action, it feels like magic. Imagine you&rsquo;re
building a dashboard that displays user profiles with their recent orders and
reviews. With a traditional REST API, you might need three or four separate
endpoints, carefully orchestrated on the frontend. With GraphQL, you simply
write a query that exactly matches your data needs:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-graphql">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-graphql" data-lang="graphql"><span class="line"><span class="cl"><span class="kd">query</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="py">user</span><span class="p">(</span><span class="py">id</span><span class="p">:</span><span class="w"> </span><span class="nc">123</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="py">name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="py">recentOrders</span><span class="p">(</span><span class="py">last</span><span class="p">:</span><span class="w"> </span><span class="nc">5</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="py">amount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="py">status</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="py">reviews</span><span class="p">(</span><span class="py">last</span><span class="p">:</span><span class="w"> </span><span class="nc">3</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="py">rating</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="py">content</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>This kind of flexibility makes frontend developers extraordinarily happy. They
get precisely the data they need, no more and no less. No more wrangling
multiple API endpoints or dealing with over-fetched data. No more adding
backend endpoints every time the frontend needs a slightly different data
shape.</p>
<p>The benefits seem to stack up quickly:</p>
<p>First, there&rsquo;s the elegant solution to the chronic problem of over-fetching.
Instead of receiving every field from your <code>User</code> model when you only need the
name, you get exactly what you asked for. In a world where mobile data usage
matters, this can seem like a compelling advantage.</p>
<p>Then there&rsquo;s the development workflow. Frontend teams can work more
independently, experimenting with different data requirements without needing
constant backend changes. They can iterate faster, and the self-documenting
nature of GraphQL schemas means they always know exactly what data is
available.</p>
<p>The single endpoint approach also feels cleaner than maintaining dozens of REST
endpoints. Rather than debating whether something should be its own endpoint or
included in an existing one, you can just add it to your schema and let clients
decide when to request it.</p>
<p>For teams juggling multiple frontend applications – perhaps a web app, a mobile
app, and a partner API – GraphQL&rsquo;s flexibility seems particularly appealing.
Each client can request its own specific data shape without requiring custom
endpoints.</p>
<p>These benefits are real, and they&rsquo;ve driven GraphQL&rsquo;s adoption at companies
dealing with complex data requirements and multiple client applications. But as
we&rsquo;ll explore in the next section, implementing and maintaining these
capabilities comes with significant costs that aren&rsquo;t immediately obvious from
the demos and documentation.</p>
<h2 id="the-real-costs-of-graphql-implementation">The real costs of GraphQL implementation</h2>
<p>While GraphQL&rsquo;s benefits shine in demos, its costs become apparent once you
start implementing it in a real Rails application. Let&rsquo;s look at what actually
happens when you add GraphQL to your typical Rails app.</p>
<p>First, there&rsquo;s the initial setup. Even with helpful gems like <code>graphql-ruby</code>,
you&rsquo;re looking at significant work to get started. Every model that needs to be
exposed requires a new type definition:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Types</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">UserType</span> <span class="o">&lt;</span> <span class="no">Types</span><span class="o">::</span><span class="no">BaseObject</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:id</span><span class="p">,</span> <span class="no">ID</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:name</span><span class="p">,</span> <span class="nb">String</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:email</span><span class="p">,</span> <span class="nb">String</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:orders</span><span class="p">,</span> <span class="o">[</span><span class="no">Types</span><span class="o">::</span><span class="no">OrderType</span><span class="o">]</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:reviews</span><span class="p">,</span> <span class="o">[</span><span class="no">Types</span><span class="o">::</span><span class="no">ReviewType</span><span class="o">]</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">orders</span>
</span></span><span class="line"><span class="cl">      <span class="no">BatchLoader</span><span class="o">::</span><span class="no">GraphQL</span><span class="o">.</span><span class="n">for</span><span class="p">(</span><span class="n">object</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="n">batch</span> <span class="k">do</span> <span class="o">|</span><span class="n">user_ids</span><span class="p">,</span> <span class="n">loader</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">          <span class="no">Order</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">user_id</span><span class="p">:</span> <span class="n">user_ids</span><span class="p">)</span><span class="o">.</span><span class="n">group_by</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">user_id</span><span class="p">,</span> <span class="n">orders</span><span class="o">|</span> <span class="n">loader</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">orders</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>See that <code>orders</code> method? That&rsquo;s handling N+1 queries, one of the first
problems you&rsquo;ll encounter. While Active Record makes it easy to
<code>includes(:orders)</code> in a regular Rails controller, with GraphQL you&rsquo;ll need to
implement batch loading for every relationship. Tools like <code>batch-loader</code> help,
but they add another layer of complexity to your codebase.</p>
<p>Authorization becomes more complex too. Instead of handling permissions in your
controllers, you now need to think about field-level authorization:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Types</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">UserType</span> <span class="o">&lt;</span> <span class="no">Types</span><span class="o">::</span><span class="no">BaseObject</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:email</span><span class="p">,</span> <span class="nb">String</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">authorize!</span> <span class="ss">:admin</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">authorize!</span><span class="p">(</span><span class="n">role</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">GraphQL</span><span class="o">::</span><span class="no">UnauthorizedError</span> <span class="k">unless</span> <span class="n">context</span><span class="o">[</span><span class="ss">:current_user</span><span class="o">]&amp;.</span><span class="n">has_role?</span><span class="p">(</span><span class="n">role</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>But the real maintenance burden comes from keeping your schema in sync with
your models. Add a field to your <code>User</code> model? You&rsquo;ll need to update the
<code>UserType</code>. Add a new relationship? That&rsquo;s a new type definition and possibly new
batch loading code. What used to be handled automatically by Active Record now
requires explicit schema updates.</p>
<p>Testing becomes more involved as well. Instead of testing REST endpoints that
map cleanly to controller actions, you&rsquo;re now testing queries and mutations
that can touch multiple parts of your schema. A single query might need to
verify authorization, resolve relationships, and handle errors across several
types:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">describe</span> <span class="s2">&#34;Users Query&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">let</span><span class="p">(</span><span class="ss">:query</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;&lt;~</span><span class="no">GRAPHQL</span>
</span></span><span class="line"><span class="cl">      <span class="n">query</span><span class="p">(</span><span class="vg">$id</span><span class="p">:</span> <span class="no">ID</span><span class="o">!</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">user</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="vg">$id</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nb">name</span>
</span></span><span class="line"><span class="cl">          <span class="n">email</span>
</span></span><span class="line"><span class="cl">          <span class="n">orders</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">amount</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="no">GRAPHQL</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="s2">&#34;returns user data with orders&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">create_list</span><span class="p">(</span><span class="ss">:order</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="ss">user</span><span class="p">:</span> <span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="no">MyAppSchema</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">query</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">variables</span><span class="p">:</span> <span class="p">{</span> <span class="nb">id</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="ss">context</span><span class="p">:</span> <span class="p">{</span> <span class="ss">current_user</span><span class="p">:</span> <span class="n">create</span><span class="p">(</span><span class="ss">:admin_user</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Now check the deeply nested response structure</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">dig</span><span class="p">(</span><span class="s2">&#34;data&#34;</span><span class="p">,</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;orders&#34;</span><span class="p">))</span><span class="o">.</span><span class="n">to</span> <span class="n">be_present</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">dig</span><span class="p">(</span><span class="s2">&#34;data&#34;</span><span class="p">,</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;orders&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">length</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">eq</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>Performance monitoring becomes trickier too. Instead of clear controller
actions that map to specific business operations, you&rsquo;re dealing with arbitrary
queries that can access your data in countless ways. Tools like Scout or New
Relic need extra configuration to make sense of GraphQL operations, and
debugging production issues often requires more context to understand exactly
what data was being requested.</p>
<p>The schema documentation, while automatic, needs constant attention. As your
schema grows, keeping the documentation clear and useful becomes its own
maintenance task. Unlike Rails&rsquo; REST conventions, which are widely understood,
GraphQL schemas need to be carefully documented to be useful to client
developers.</p>
<p>These costs compound over time. Each new feature adds complexity to your
schema, each new relationship needs careful performance consideration, and each
new team member needs to understand not just Rails conventions, but your
specific GraphQL implementation choices.</p>
<h2 id="rails-built-in-solutions-are-often-sufficient">Rails&rsquo; built-in solutions are often sufficient</h2>
<p>One of Rails&rsquo; greatest strengths has always been its sensible defaults and
conventional solutions. While these might seem boring compared to GraphQL&rsquo;s
flexibility, they often solve the same problems with significantly less
complexity.</p>
<p>Let&rsquo;s tackle the common problems that drive teams to GraphQL, and look at how
Rails already solves them:</p>
<p><strong>&ldquo;But we need to avoid N+1 queries!&rdquo;</strong></p>
<p>Rails has had a solution for this since the beginning. Active Record&rsquo;s eager
loading is both powerful and easy to use:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">index</span>
</span></span><span class="line"><span class="cl">  <span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:orders</span><span class="p">,</span> <span class="ss">:reviews</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">active</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">limit</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="n">users</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p><strong>&ldquo;But we need flexible response shapes!&rdquo;</strong></p>
<p>Rails serializers have you covered. Using a gem like <code>active_model_serializers</code>
or even just plain Ruby objects, you can create multiple representations of
your data:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserSerializer</span> <span class="o">&lt;</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Serializer</span>
</span></span><span class="line"><span class="cl">  <span class="n">attributes</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:name</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">attribute</span> <span class="ss">:full_data</span><span class="p">,</span> <span class="k">if</span><span class="p">:</span> <span class="ss">:admin?</span>
</span></span><span class="line"><span class="cl">  <span class="n">has_many</span> <span class="ss">:recent_orders</span>
</span></span><span class="line"><span class="cl">  <span class="n">has_many</span> <span class="ss">:recent_reviews</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">recent_orders</span>
</span></span><span class="line"><span class="cl">    <span class="n">object</span><span class="o">.</span><span class="n">orders</span><span class="o">.</span><span class="n">last</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">admin?</span>
</span></span><span class="line"><span class="cl">    <span class="n">current_user</span><span class="o">.</span><span class="n">admin?</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p><strong>&ldquo;But we need to combine multiple resources!&rdquo;</strong></p>
<p>Rails controllers can handle this elegantly:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">dashboard</span>
</span></span><span class="line"><span class="cl">  <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:orders</span><span class="p">,</span> <span class="ss">:reviews</span><span class="p">)</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="ss">user</span><span class="p">:</span> <span class="no">UserSerializer</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="ss">recent_activity</span><span class="p">:</span> <span class="no">ActivitySerializer</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">recent_activity</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="ss">account_summary</span><span class="p">:</span> <span class="no">AccountSerializer</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">account</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p><strong>&ldquo;But what about caching?&rdquo;</strong></p>
<p>Rails&rsquo; caching is battle-tested and works great with JSON endpoints:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">index</span>
</span></span><span class="line"><span class="cl">    <span class="n">orders</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="o">[</span><span class="s2">&#34;orders&#34;</span><span class="p">,</span> <span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]]</span><span class="p">,</span> <span class="ss">expires_in</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">hour</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Order</span><span class="o">.</span><span class="n">page</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]</span><span class="p">)</span><span class="o">.</span><span class="n">to_json</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="n">orders</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p><strong>&ldquo;But we need versioning!&rdquo;</strong></p>
<p>Rails has conventional patterns for API versioning that are well understood:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Api</span>
</span></span><span class="line"><span class="cl">  <span class="k">module</span> <span class="nn">V1</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># V1 implementation</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">module</span> <span class="nn">V2</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># V2 implementation with new fields</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p><strong>&ldquo;But our frontend needs different data shapes for different screens!&rdquo;</strong></p>
<p>Instead of one massive GraphQL schema, consider scope-specific endpoints that
map to your UI needs:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Api</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">UserProfileController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">show</span>
</span></span><span class="line"><span class="cl">      <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:profile_data</span><span class="p">)</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="no">UserProfileSerializer</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">UserDashboardController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">show</span>
</span></span><span class="line"><span class="cl">      <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:dashboard_data</span><span class="p">)</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="no">UserDashboardSerializer</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>The beauty of these Rails solutions isn&rsquo;t just their simplicity – it&rsquo;s that
they compose well together and follow patterns that any Rails developer can
understand. They leverage Rails&rsquo; performance optimizations, work seamlessly
with the asset pipeline and caching, and integrate naturally with the rest of
your Rails tooling.</p>
<p>Most importantly, these solutions scale with your application&rsquo;s actual needs.
Need more complex data fetching? Add a query object. Need more sophisticated
caching? Rails&rsquo; caching framework can handle it. Need to optimize payload size?
Use <code>jbuilder</code> or custom serializers to send exactly what you need.</p>
<h2 id="hidden-operational-costs">Hidden operational costs</h2>
<p>While we&rsquo;ve discussed the technical implementation costs of GraphQL, there&rsquo;s a
whole category of operational costs that often go unconsidered until they hit
your team in production. Let&rsquo;s explore these hidden costs that can
significantly impact your team&rsquo;s velocity and operational efficiency.</p>
<h3 id="the-learning-curve-tax">The learning curve tax</h3>
<p>Every new Rails developer knows what <code>users_controller#show</code> does. But drop a
new developer into a GraphQL codebase, and they&rsquo;re facing questions like:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># What&#39;s the difference between these again?</span>
</span></span><span class="line"><span class="cl"><span class="n">field</span> <span class="ss">:total</span><span class="p">,</span> <span class="nb">Integer</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="n">field</span> <span class="ss">:total</span><span class="p">,</span> <span class="nb">Integer</span><span class="p">,</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl"><span class="n">field</span> <span class="ss">:total</span><span class="p">,</span> <span class="nb">Integer</span>  <span class="c1"># Wait, what&#39;s the default?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Why isn&#39;t this working?</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">orders</span>
</span></span><span class="line"><span class="cl">  <span class="n">object</span><span class="o">.</span><span class="n">orders</span>  <span class="c1"># Oops, N+1 query!</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># How do I handle authorization here?</span>
</span></span><span class="line"><span class="cl"><span class="n">field</span> <span class="ss">:secret_data</span><span class="p">,</span> <span class="nb">String</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Is this the right place for auth?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>Even experienced Rails developers need time to become productive with GraphQL.
This learning curve isn&rsquo;t just about syntax – it&rsquo;s about understanding resolver
patterns, type systems, schema design, and performance implications. While your
senior developers might pick it up quickly, you&rsquo;re adding complexity for every
new hire and junior developer.</p>
<h3 id="the-debugging-tax">The debugging tax</h3>
<p>When something goes wrong in a REST endpoint, the debugging process is
straightforward: check the logs, find the controller action, and follow the
stack trace. With GraphQL, debugging becomes more complex:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Your logs might show this query:</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">user</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="s2">&#34;123&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">orders</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">items</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">product</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">price</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># But where&#39;s the N+1 query coming from?</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Is it in the orders resolver?</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The items resolver?</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The product resolver?</span></span></span></code></pre></div></div>
<p>Your APM tools like New Relic or Scout need additional configuration to make
sense of GraphQL operations. Stack traces become less helpful because the entry
point is always your GraphQL engine. And good luck trying to quickly reproduce
an issue when you need to reconstruct the exact query that caused it.</p>
<h3 id="the-documentation-tax">The documentation tax</h3>
<p>With REST, your API documentation often follows naturally from your controller
actions and serializers. With GraphQL, maintaining clear documentation becomes
a constant task:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Types</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">OrderType</span> <span class="o">&lt;</span> <span class="no">Types</span><span class="o">::</span><span class="no">BaseObject</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:status</span><span class="p">,</span> <span class="nb">String</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">      <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">description</span><span class="p">:</span> <span class="s2">&#34;Order status (pending, processing, shipped, delivered)&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:total</span><span class="p">,</span> <span class="nb">Integer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">description</span><span class="p">:</span> <span class="s2">&#34;Order total in cents&#34;</span>
</span></span><span class="line"><span class="cl">      
</span></span><span class="line"><span class="cl">    <span class="c1"># Multiply this by every field in your schema...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>While GraphQL is self-documenting in theory, in practice you need to maintain
descriptions, deprecation notices, and usage examples. This documentation needs
to be kept in sync with your implementation, and it needs to be detailed enough
for client developers to use effectively.</p>
<h3 id="the-testing-tax">The testing tax</h3>
<p>Testing complexity increases exponentially with GraphQL. Instead of testing
discrete controller actions, you&rsquo;re testing combinations of fields and
resolvers:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># A simple REST controller test</span>
</span></span><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">describe</span> <span class="no">UsersController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="s2">&#34;returns user data&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">get</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">params</span><span class="p">:</span> <span class="p">{</span> <span class="nb">id</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">be_successful</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">json_response</span><span class="o">[</span><span class="ss">:name</span><span class="o">]</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">eq</span> <span class="n">user</span><span class="o">.</span><span class="n">name</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># The equivalent GraphQL test</span>
</span></span><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">describe</span> <span class="no">Types</span><span class="o">::</span><span class="no">QueryType</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">let</span><span class="p">(</span><span class="ss">:query</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;&lt;~</span><span class="no">GQL</span>
</span></span><span class="line"><span class="cl">      <span class="n">query</span><span class="p">(</span><span class="vg">$id</span><span class="p">:</span> <span class="no">ID</span><span class="o">!</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">user</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="vg">$id</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nb">name</span>
</span></span><span class="line"><span class="cl">          <span class="n">orders</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">status</span>
</span></span><span class="line"><span class="cl">            <span class="n">items</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="n">product</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nb">name</span>
</span></span><span class="line"><span class="cl">                <span class="n">price</span>
</span></span><span class="line"><span class="cl">              <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="no">GQL</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="s2">&#34;returns nested user data&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">execute_query</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="ss">variables</span><span class="p">:</span> <span class="p">{</span> <span class="nb">id</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Now check the deeply nested response structure</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Don&#39;t forget to test authorization at each level</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># And error handling</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># And N+1 query protection</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<h3 id="the-production-support-tax">The production support tax</h3>
<p>When production issues occur, the complexity of GraphQL makes them harder to
diagnose and fix:</p>
<ul>
<li>Performance problems are harder to isolate because a single query might touch
dozens of resolvers</li>
<li>Error reporting becomes more complex because errors can occur at multiple
levels of the query</li>
<li>Capacity planning is trickier because client queries can be arbitrarily
complex</li>
<li>Query complexity limits and timeouts need careful tuning to prevent DOS
vulnerabilities</li>
</ul>
<p>Each of these costs might seem manageable in isolation, but they compound over
time and across team size. A five-person team might handle these overhead
costs, but as your team grows, these costs scale with each new developer, each
new feature, and each new client application.</p>
<h2 id="when-graphql-makes-sense-and-when-it-doesnt">When GraphQL makes sense (and when it doesn&rsquo;t)</h2>
<p>After all this criticism of GraphQL, you might be wondering if there&rsquo;s ever a
right time to use it. The answer is yes – GraphQL can be the right choice, but
the conditions need to justify its complexity.</p>
<h3 id="when-graphql-might-be-worth-the-cost">When GraphQL might be worth the cost</h3>
<p>You should consider GraphQL when your application has:</p>
<ul>
<li>
<p><strong>Multiple, significantly different clients</strong>: If you&rsquo;re building separate web,
mobile, and partner API experiences that each need very different data shapes,
GraphQL&rsquo;s flexibility becomes valuable. GitHub is a perfect example – they
serve their web UI, mobile apps, and third-party integrations all from the same
GraphQL API.</p>
</li>
<li>
<p><strong>Complex, nested data requirements</strong>: If your frontend frequently needs to fetch
deeply nested, interconnected data that would require multiple REST roundtrips,
GraphQL can simplify this orchestration. Think social networks where you need
user profiles, their posts, comments on those posts, and profiles of users who
commented – all in one view.</p>
</li>
<li>
<p><strong>A large, dedicated API team</strong>: When you have the resources to properly
maintain, monitor, and optimize a GraphQL implementation, its benefits become
more achievable. Companies like Shopify can justify GraphQL because they have
teams dedicated to their API infrastructure.</p>
</li>
</ul>
<h3 id="when-to-skip-graphql">When to skip GraphQL</h3>
<p>Stick with Rails&rsquo; conventional REST API patterns when:</p>
<ul>
<li>Your application is primarily CRUD: If most of your endpoints map cleanly to
resource actions (create, read, update, delete), REST already handles this
perfectly. Adding GraphQL would be overengineering.</li>
</ul>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># This is all you need for most cases</span>
</span></span><span class="line"><span class="cl"><span class="n">resources</span> <span class="ss">:orders</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">resources</span> <span class="ss">:line_items</span>
</span></span><span class="line"><span class="cl">  <span class="n">member</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span> <span class="ss">:refund</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span> <span class="ss">:ship</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<ul>
<li>
<p>Your data shape is relatively stable: If your frontend needs aren&rsquo;t
constantly changing and you don&rsquo;t have radically different client requirements,
REST endpoints with well-designed serializers will serve you well.</p>
</li>
<li>
<p>You have a small to medium team: If you can&rsquo;t dedicate significant
engineering resources to GraphQL infrastructure, the maintenance burden will
likely outweigh the benefits.</p>
</li>
</ul>
<h3 id="making-the-decision">Making the decision</h3>
<p>Before adopting GraphQL, ask yourself:</p>
<ol>
<li>Can Rails&rsquo; built-in tools solve our current problems?</li>
<li>Do we have the engineering resources to properly implement and maintain GraphQL?</li>
<li>Are our API requirements complex enough to justify the overhead?</li>
<li>Have we measured the actual performance impact of our current REST implementation?</li>
</ol>
<p>Remember, you can always start with a REST API and add GraphQL later if needed.
Many teams have found success with a hybrid approach – using REST for simple
CRUD operations and GraphQL for more complex data requirements.</p>
<h3 id="final-thoughts">Final thoughts</h3>
<p>GraphQL isn&rsquo;t bad technology – it&rsquo;s just often misapplied. For many Rails
applications, the conventional REST approach, combined with thoughtful use of
Rails&rsquo; built-in tools, provides a simpler, more maintainable solution. Before
jumping on the GraphQL bandwagon, make sure its benefits truly outweigh its
considerable carrying costs for your specific use case.</p>
<blockquote>
<p>🔍 In software development, simpler solutions tend to lead to happier teams
and more maintainable codebases in the long run. Sometimes the &ldquo;boring&rdquo;
solution is exactly what your application needs.</p>
</blockquote>
]]></description></item><item><title>SOLID++: Encapsulate What Varies</title><link>https://tracyatteberry.com/posts/ewv/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/ewv/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/ewv/cocoon.jpg" referrerpolicy="no-referrer">
            </div><h3 id="introduction-organizing-your-tools">Introduction: Organizing Your Tools</h3>
<p>Ever notice how the messiest parts of your code are often where you&rsquo;re
handling different cases or variations? Maybe it&rsquo;s different file formats,
payment methods, or shipping calculations. That&rsquo;s exactly what we&rsquo;re going to
tackle today with one of the most practical object-oriented design principles:
&lsquo;Encapsulate what varies.&rsquo;</p>
<p>Think of it like organizing your tools. Instead of having screwdrivers and
wrenches scattered throughout the shop and house, you put them all in one
toolbox. Now, when you need to change or add a tool, you know exactly where
to go. That&rsquo;s what we mean by encapsulation in code – taking the parts that
might change and putting them in a well-defined place. Okay, yes. There is
that one screwdriver that always ends up in the kitchen drawer, but sometimes
a little duplication is better than an unhappy spouse.</p>
<p>In this article, we&rsquo;ll look at how this principle can transform messy,
hard-to-maintain code into something clean and flexible. I&rsquo;ll show you real
examples in Ruby, and by the end, you&rsquo;ll have a new tool for keeping your code
organized and easy to change.</p>
<h3 id="the-core-principle-explained">The Core Principle Explained</h3>
<p>Encapsulate what varies is all about isolating the parts of your code that
might change. It&rsquo;s a simple idea, but it can have a big impact on how
you structure your code. By putting the things that vary in one place,
you make your code more flexible and easier to maintain.</p>
<p>Imagine you&rsquo;re working on an woodworking platform that sells many types of
dressers. Each dresser can be of a different type and have a different assembly
process for one kind of dresser. If you scatter these variations throughout
your code, or conflate them with other code you&rsquo;ll end up with a tangled mess
that&rsquo;s hard to understand and change.</p>
<p>By encapsulating what varies, you can isolate these differences in one place.
This makes your code easier to read, test, and extend. When you need to add a
new dresser type or change the assembly logic, you know exactly where to look.</p>
<h3 id="a-practical-example">A Practical Example</h3>
<blockquote>
<p>🔍 In this post, we&rsquo;ll use Ruby 3.1 for code examples with endless methods
for brevity, but the principles here apply to any object-oriented language.</p>
</blockquote>
<p>Here&rsquo;s a simple example to illustrate the concept. Suppose you have a class
that builds a dresser based on the dresser type. You could write a single
method that handles all these variations:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DresserBuilder</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">construct</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">type</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:armoire</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span> <span class="o">=</span> <span class="no">Armoire</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:highboy</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span> <span class="o">=</span> <span class="no">Highboy</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:bombe</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span> <span class="o">=</span> <span class="no">Bombe</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">&#34;Unknown dresser type: </span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">select_wood</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">cut_pieces</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="ss">:armoire</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span><span class="o">.</span><span class="n">join_traditionally</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span><span class="o">.</span><span class="n">join_with_screws</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">finish</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">puts</span> <span class="s2">&#34;Dresser built!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Usage</span>
</span></span><span class="line"><span class="cl"><span class="n">armoire</span> <span class="o">=</span> <span class="no">DresserBuilder</span><span class="o">.</span><span class="n">construct</span><span class="p">(</span><span class="ss">:armoire</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Dresser built!</span></span></span></code></pre></div></div>
<p>This code works, but it&rsquo;s not very flexible. What if you need to add a new
dresser type or change the assembly process for one kind of dresser? You&rsquo;d have
to modify this method, which violates the Open/Closed Principle of SOLID.</p>
<p>Let&rsquo;s apply the &rsquo;encapsulate what varies&rsquo; principle to this code. We&rsquo;ll create
a separate<code>DresserFactory</code> class to handle the creation of different dresser
types. This class will encapsulate the choice of dresser type and return the
appropriate dresser object:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DresserFactory</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">make</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">type</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:armoire</span>
</span></span><span class="line"><span class="cl">      <span class="no">Armoire</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:highboy</span>
</span></span><span class="line"><span class="cl">      <span class="no">Highboy</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="ss">:bombe</span>
</span></span><span class="line"><span class="cl">      <span class="no">Bombe</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">&#34;Unknown dresser type: </span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DresserBuilder</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">construct</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span> <span class="o">=</span> <span class="no">DresserFactory</span><span class="o">.</span><span class="n">make</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">select_wood</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">cut_pieces</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="ss">:armoire</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span><span class="o">.</span><span class="n">join_traditionally</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">dresser</span><span class="o">.</span><span class="n">join_with_screws</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">finish</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">puts</span> <span class="s2">&#34;Dresser built!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Usage</span>
</span></span><span class="line"><span class="cl"><span class="n">armoire</span> <span class="o">=</span> <span class="no">DresserBuilder</span><span class="o">.</span><span class="n">construct</span><span class="p">(</span><span class="ss">:armoire</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Dresser built!</span></span></span></code></pre></div></div>
<p>Now we can easily add new types without modifying the <code>Dresser</code> class. This is
better, but what if the assembly process varies between dresser types? You&rsquo;d
have to modify the <code>Dresser</code> class again.</p>
<h4 id="strategy-pattern">Strategy Pattern</h4>
<p>Let&rsquo;s take it a step further and use the Strategy pattern to encapsulate the
assembly process in separate classes. This way, each dresser type can have its
own assembly logic, and we can easily change that without changing the
<code>Dresser</code> class:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DresserFactory</span>
</span></span><span class="line"><span class="cl">  <span class="no">BLUEPRINTS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="ss">armoire</span><span class="p">:</span> <span class="no">Armoire</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">highboy</span><span class="p">:</span> <span class="no">Highboy</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">bombe</span><span class="p">:</span> <span class="no">Bombe</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">make</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">BLUEPRINTS</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="n">type</span><span class="p">)</span> <span class="p">{</span> 
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">&#34;Unknown dresser type: </span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Assembleable</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">assemble</span> <span class="o">=</span> <span class="k">raise</span> <span class="no">NotImplementedError</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">AbstractDresser</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Assembleable</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Armoire</span> <span class="o">&lt;</span> <span class="no">AbstractDresser</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">assemble</span>
</span></span><span class="line"><span class="cl">    <span class="n">select_wood</span>
</span></span><span class="line"><span class="cl">    <span class="n">cut_pieces</span>
</span></span><span class="line"><span class="cl">    <span class="n">join_traditionally</span>
</span></span><span class="line"><span class="cl">    <span class="n">finish_with_lacquer</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">private</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">select_wood</span> <span class="o">=</span> <span class="nb">puts</span> <span class="s2">&#34;Selecting wood for armoire&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">cut_pieces</span> <span class="o">=</span> <span class="nb">puts</span> <span class="s2">&#34;Cutting pieces for armoire&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">join_traditionally</span> <span class="o">=</span> <span class="nb">puts</span> <span class="s2">&#34;Assembling armoire traditionally&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">finish_with_lacquer</span> <span class="o">=</span> <span class="nb">puts</span> <span class="s2">&#34;Finishing armoire with lacquer&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DresserBuilder</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">construct</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span> <span class="o">=</span> <span class="no">DresserFactory</span><span class="o">.</span><span class="n">make</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span><span class="o">.</span><span class="n">assemble</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">puts</span> <span class="s2">&#34;Dresser built!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">dresser</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Usage</span>
</span></span><span class="line"><span class="cl"><span class="n">armoire</span> <span class="o">=</span> <span class="no">DresserBuilder</span><span class="o">.</span><span class="n">construct</span><span class="p">(</span><span class="ss">:armoire</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Selecting wood for armoire</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Cutting pieces for armoire</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Assembling armoire traditionally</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Finishing armoire with lacquer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; Dresser built!</span></span></span></code></pre></div></div>
<p>Now, each dresser type has its own class, and the <code>Dresser</code> class delegates the
assembly process to the appropriate type. We&rsquo;ve also taken this opportunity to
update the <code>DresserFactory</code> class to trade the <code>case</code> statement for a hash
of blueprints for each dresser type. This way, adding a new type is as simple
as adding a new entry to the hash.</p>
<h3 id="common-patterns-that-apply-this-principle">Common Patterns That Apply This Principle</h3>
<p>The &rsquo;encapsulate what varies&rsquo; principle is closely related to several design
patterns that help you manage change in your code. Above, we used the Strategy
pattern to encapsulate the assembly process for different dresser types and the
Factory Method pattern to create instances of these types.</p>
<h3 id="tips-for-identifying-varying-elements">Tips for Identifying Varying Elements</h3>
<p>When applying the &rsquo;encapsulate what varies&rsquo; principle, it&rsquo;s essential to
identify the parts of your code that are likely to change. Here are some tips
to help you spot these elements:</p>
<ul>
<li><strong>Look for conditional logic</strong>: If you have a lot of <code>if</code> statements or
<code>case</code> blocks that handle different cases, those are good candidates for
encapsulation.</li>
<li><strong>Think about future requirements</strong>: Consider what might change in the future
and how you can isolate those changes.</li>
<li><strong>Separate concerns</strong>: If you have different concerns mixed together in one
class or method, try to separate them into distinct components.</li>
<li><strong>Code analysis</strong>: Use tools like static code analysis or code reviews to
identify parts of your code that change frequently.</li>
</ul>
<h3 id="common-pitfalls">Common Pitfalls</h3>
<p>While the &rsquo;encapsulate what varies&rsquo; principle is a powerful tool for managing
change, there are some common pitfalls to watch out for:</p>
<ul>
<li><strong>Overengineering</strong>: Don&rsquo;t go overboard and create unnecessary abstractions.
Keep it simple and refactor only when it makes sense.</li>
<li><strong>Premature optimization</strong>: Don&rsquo;t try to predict every possible change upfront.</li>
<li><strong>Ignoring SOLID principles</strong>: Remember that encapsulation is just one part
of good object-oriented design. Make sure your code follows other principles
like the Single Responsibility Principle and the Open/Closed Principle.</li>
<li><strong>Not testing variations</strong>: When you encapsulate what varies, make sure to
test each variation thoroughly. It&rsquo;s easy to introduce bugs when you refactor
code, so testing is crucial.</li>
</ul>
<p>Keep these pitfalls in mind as you apply this principle in your code. With
practice, you&rsquo;ll get better at identifying the parts that might change and
encapsulating them effectively.</p>
<h3 id="conclusion">Conclusion</h3>
<p>The &rsquo;encapsulate what varies&rsquo; principle is a powerful tool for managing change
in your code. By isolating the parts that are likely change, you make your code
more flexible, maintainable, and testable. Whether you&rsquo;re working on an
e-commerce platform, a game engine, or a woodworking platform, this principle
can help you keep your code organized and easy to extend.</p>
<p>And yes, sometimes that screwdriver will still end up in the kitchen drawer, but
sometimes that&rsquo;s just where it needs to be to maintain household harmony.</p>
<blockquote>
<p>🔍 This post is part of the <a href="/posts/solid_plus_plus" rel="">SOLID++</a> series where
we are exploring principles of object-oriented design beyond the five SOLID
principles.</p>
</blockquote>
]]></description></item></channel></rss>