<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Travis Wilson</title>
    <description>The latest articles on Forem by Travis Wilson (@traviticus).</description>
    <link>https://forem.com/traviticus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3704751%2F5db573b5-f8d6-4ffd-bf6a-3641ccb17325.png</url>
      <title>Forem: Travis Wilson</title>
      <link>https://forem.com/traviticus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/traviticus"/>
    <language>en</language>
    <item>
      <title>I Deleted 100,000 Lines of Code to Pivot My App to Workflows (And Made It 37x Faster)</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Thu, 05 Feb 2026 20:28:26 +0000</pubDate>
      <link>https://forem.com/traviticus/i-deleted-100000-lines-of-code-to-pivot-my-app-to-workflows-and-made-it-37x-faster-2f57</link>
      <guid>https://forem.com/traviticus/i-deleted-100000-lines-of-code-to-pivot-my-app-to-workflows-and-made-it-37x-faster-2f57</guid>
      <description>&lt;p&gt;Two weeks ago, I had a data pipeline platform with separate Import and Export systems.&lt;/p&gt;

&lt;p&gt;Today, I have a workflow engine. And it's 37x faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr4ifav72xlx02s6e3u5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr4ifav72xlx02s6e3u5.png" alt="Gitlab Change Counts" width="526" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net result: ~106,000 lines deleted. 2.5 minute operations now take 4 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Market Told Me Something
&lt;/h2&gt;

&lt;p&gt;I kept seeing the same thing on X: people sharing screenshots of their n8n workflows. Visual canvases with nodes and connections. "Here's how I automated my entire marketing pipeline." "Here's my AI agent workflow."&lt;/p&gt;

&lt;p&gt;And I realized: that's what people want. Not "data pipelines"—&lt;em&gt;workflows&lt;/em&gt;. Visual, connectable, shareable.&lt;/p&gt;

&lt;p&gt;My platform could import data from S3, transform it, export to BigQuery. Powerful stuff. But nobody's posting screenshots of an import configuration form.&lt;/p&gt;

&lt;p&gt;I wanted Flywheel to be the thing people screenshot and share. That meant pivoting from "data pipeline tool" to "workflow automation platform."&lt;/p&gt;

&lt;p&gt;The problem? My entire architecture was built around "Import" and "Export" as separate concepts. Not nodes on a canvas. Not a visual workflow you'd want to show off.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture I Had
&lt;/h2&gt;

&lt;p&gt;Two complete systems, doing almost the same thing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Import System&lt;/th&gt;
&lt;th&gt;Export System&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imports/handler.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exports/handler.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imports/service.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exports/service.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imports/dao.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exports/dao.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imports/model.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exports/model.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imports/subscriptions.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exports/subscriptions.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus, for each of 8 data providers (S3, BigQuery, Postgres, DynamoDB, Firestore, GCS, Pub/Sub, Domo):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Import Code&lt;/th&gt;
&lt;th&gt;Export Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{provider}/chunk_fetcher.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{provider}/export_writer.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{provider}/subscriptions.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{provider}/export_subscriptions.go&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;That's 16 files per provider just for the import/export split.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the kicker? They couldn't talk to each other. Want to move data from S3 directly to BigQuery? Can't. You had to import to my system first, then export out. Two operations. Two sets of overhead. Two points of failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pivot
&lt;/h2&gt;

&lt;p&gt;What if Import and Export were just... different nodes in the same workflow?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old mental model:
  Import (External → Flywheel)
  Export (Flywheel → External)

New mental model:
  Source (read from anywhere) → Sink (write to anywhere)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A "Source" doesn't care if it's S3 or my internal storage. A "Sink" doesn't care if it's BigQuery or a file download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import&lt;/strong&gt; = &lt;code&gt;Source(S3) → Sink(Flywheel)&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Export&lt;/strong&gt; = &lt;code&gt;Source(Flywheel) → Sink(BigQuery)&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Direct ETL&lt;/strong&gt; = &lt;code&gt;Source(S3) → Sink(BigQuery)&lt;/code&gt; ← &lt;em&gt;couldn't do this before&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One unified workflow system. Not two separate systems pretending to be related.&lt;/p&gt;


&lt;h2&gt;
  
  
  Two Weeks, 1,310 Files
&lt;/h2&gt;

&lt;p&gt;I had no users yet. No backward compatibility. No "gradual migration."&lt;/p&gt;

&lt;p&gt;So I did the only sane thing: burn it down and rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: The Deletion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleted entire &lt;code&gt;imports/&lt;/code&gt; package (~5,500 lines)&lt;/li&gt;
&lt;li&gt;Deleted entire &lt;code&gt;exports/&lt;/code&gt; package (~5,900 lines)&lt;/li&gt;
&lt;li&gt;Deleted entire &lt;code&gt;egress/&lt;/code&gt; package (~1,000 lines)&lt;/li&gt;
&lt;li&gt;Deleted per-provider import/export code across 8 providers&lt;/li&gt;
&lt;li&gt;Deleted corresponding frontend: &lt;code&gt;ImportWizard&lt;/code&gt;, &lt;code&gt;ExportWizard&lt;/code&gt;, &lt;code&gt;ImportList&lt;/code&gt;, &lt;code&gt;ExportList&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By end of week 1, the app didn't compile. That's fine. Dead code can't hurt you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2: The Rebuild&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created unified &lt;code&gt;pipelines/&lt;/code&gt; package with orchestrator&lt;/li&gt;
&lt;li&gt;Created &lt;code&gt;conditions/&lt;/code&gt; package for filtering&lt;/li&gt;
&lt;li&gt;Implemented Source/Sink capabilities for all 8 providers&lt;/li&gt;
&lt;li&gt;Built React Flow canvas editor for visual workflow building&lt;/li&gt;
&lt;li&gt;Wrote 700+ lines of architecture documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new system has &lt;strong&gt;more functionality&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct ETL (Source → Sink, no intermediate storage)&lt;/li&gt;
&lt;li&gt;Multi-source joins&lt;/li&gt;
&lt;li&gt;Conditional routing&lt;/li&gt;
&lt;li&gt;Enrichment nodes (database lookups mid-workflow)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things that were architecturally impossible before fell out naturally from the unified model.&lt;/p&gt;


&lt;h2&gt;
  
  
  The 37x Speedup (This One Was Intentional)
&lt;/h2&gt;

&lt;p&gt;The old architecture wasn't just duplicated—it was chaos.&lt;/p&gt;

&lt;p&gt;Every provider had its own Pub/Sub consumer. Every provider had its own publisher. Messages were flying &lt;em&gt;everywhere&lt;/em&gt;. Import triggers Export triggers chunk processing triggers completion handlers triggers cleanup.&lt;/p&gt;

&lt;p&gt;I couldn't test it. I couldn't reason about it. I'd add a new provider and spend half my time debugging message flows that worked fine until they didn't.&lt;/p&gt;

&lt;p&gt;The stress test told the story:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old system:&lt;/strong&gt; 2.5 minutes for 10K records&lt;br&gt;
&lt;strong&gt;New system:&lt;/strong&gt; 4 seconds for 10K records&lt;/p&gt;

&lt;p&gt;The fix was simple: &lt;strong&gt;one orchestrator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of providers talking to each other through a spaghetti of Pub/Sub messages, the orchestrator controls everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old: Provider A publishes → Queue → Provider B subscribes → publishes → Queue → ...
New: Orchestrator calls Provider A, gets data, calls Provider B, done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator knows the full workflow graph. It executes nodes in topological order. It handles errors in one place. It tracks progress in one place.&lt;/p&gt;

&lt;p&gt;No more "which subscriber handles this message?" No more "why did this workflow stall?" No more "I changed one handler and broke three unrelated providers."&lt;/p&gt;

&lt;p&gt;The 37x speedup came from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No message queue overhead&lt;/strong&gt; - Direct function calls instead of Pub/Sub round-trips&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No intermediate storage&lt;/strong&gt; - Stream directly from source to sink&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No duplicate work&lt;/strong&gt; - One chunking strategy, not two&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I didn't optimize. I centralized control. Performance followed.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Made This Possible
&lt;/h2&gt;

&lt;p&gt;1,310 files in 2 weeks is not human-speed refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern Enforcement
&lt;/h3&gt;

&lt;p&gt;Every provider needed the same structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// provider.go - capability registration&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;S3Provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sink"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// source.go - Reader implementation&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;S3Provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// sink.go - Writer implementation&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;S3Provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude ensured all 8 providers followed this exactly. No "slightly different" implementations that would bite me later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blast Radius Mapping
&lt;/h3&gt;

&lt;p&gt;Before deleting anything, Claude identified every file that referenced imports or exports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;47 files in the core packages&lt;/li&gt;
&lt;li&gt;64 provider-specific files&lt;/li&gt;
&lt;li&gt;89 test files&lt;/li&gt;
&lt;li&gt;200+ frontend components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing got orphaned. Nothing got forgotten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Migration
&lt;/h3&gt;

&lt;p&gt;Every deleted test had a corresponding new test. The test suite never went red during the refactor. When &lt;code&gt;imports/service_test.go&lt;/code&gt; got deleted, equivalent coverage existed in &lt;code&gt;pipelines/service_test.go&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Shipped
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node Type&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read from S3, Postgres, BigQuery, Firestore, GCS, DynamoDB, Pub/Sub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modify records with JSONata expressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enrichment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Look up additional data from databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Join&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Combine records from multiple sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Write to any of the above + file downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Users drag nodes onto a canvas, connect them, run the workflow. No "configure import then configure export" friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Deletion enables features.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The old architecture couldn't do direct ETL. It couldn't do joins. It couldn't do conditional routing. Not because I didn't want those features—because two separate systems couldn't coordinate them.&lt;/p&gt;

&lt;p&gt;Unifying Import and Export into Source and Sink made all of that trivial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed systems are a last resort.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every provider having its own consumer/publisher felt "scalable." In practice, it was untestable spaghetti. One orchestrator with direct control is simpler, faster, and actually works.&lt;/p&gt;

&lt;p&gt;Don't distribute until you have to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early stage is the cheapest time to pivot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This refactor would be impossible with real users. It was a complete model change. Database schema changes. API changes. Frontend changes.&lt;/p&gt;

&lt;p&gt;If you're pre-launch and your architecture doesn't match your market positioning, now is when you fix it. The cost only goes up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself &lt;a href="https://flywheeletl.io" rel="noopener noreferrer"&gt;Flywheel&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If you're sitting on an architecture that doesn't match what users actually want:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask if your abstractions match user mental models.&lt;/strong&gt; "Import/Export" was my mental model. "Workflow" was theirs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Look for duplicate systems.&lt;/strong&gt; If you built the same thing twice with different names, you can probably unify them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Question your distributed architecture.&lt;/strong&gt; If you can't easily test or reason about message flows, you probably over-distributed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early stage = cheap pivots.&lt;/strong&gt; No users means no migration. Just delete and rebuild.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes the answer is mass deletion.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you ever pivoted your app's architecture to match market language? What triggered the change?&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI Finally Lets Startups Sweat the Small Stuff</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Mon, 19 Jan 2026 14:11:25 +0000</pubDate>
      <link>https://forem.com/traviticus/ai-finally-lets-startups-sweat-the-small-stuff-15di</link>
      <guid>https://forem.com/traviticus/ai-finally-lets-startups-sweat-the-small-stuff-15di</guid>
      <description>&lt;p&gt;Every startup has a graveyard of "we'll fix it later" decisions.&lt;/p&gt;

&lt;p&gt;The animation that's slightly janky. The form that takes 4 clicks when it should take 2. The 500 utility functions that all do basically the same thing. The config system that requires updating 8 files to add one option.&lt;/p&gt;

&lt;p&gt;We tell ourselves it's fine. Ship now, polish later. Move fast, break things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Later never comes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, you end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A codebase where fixing one bug creates two more&lt;/li&gt;
&lt;li&gt;A "standards" doc that's really just a list of exceptions&lt;/li&gt;
&lt;li&gt;3am alerts because the nightly job is running again and the DB is at 100% CPU&lt;/li&gt;
&lt;li&gt;Engineers who spend more time understanding old code than writing new code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've watched this happen at every company I've worked at. The technical debt compounds until "later" would take months.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Changed What "Later" Means
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://www.flywheeletl.io" rel="noopener noreferrer"&gt;Flywheel&lt;/a&gt; solo using an AI-native codebase approach. Here's what's different:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I actually do the small things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because I'm more disciplined. Because I finally have time.&lt;/p&gt;

&lt;p&gt;Yesterday I spent 20 minutes tweaking a login page layout. Adjusting the transparency on a divider. Making the container width responsive to the form state. Small stuff.&lt;/p&gt;

&lt;p&gt;A week ago I deleted 5,600 lines of over-engineered code and replaced it with 20 lines of config. Not because I had spare time - because Claude helped me systematically update 194 files without missing anything.&lt;/p&gt;

&lt;p&gt;Last month I shipped a &lt;a href="https://www.flywheeletl.io/blog/two-week-redesign" rel="noopener noreferrer"&gt;complete visual redesign in two weeks&lt;/a&gt;. New landing page, refined color palette, updated workflows across the entire application. That's a quarter's worth of work at most companies.&lt;/p&gt;

&lt;p&gt;These aren't "nice to haves" I'm deferring. They're getting done &lt;em&gt;now&lt;/em&gt;, while the context is fresh and the cost is low.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwehs1f29lvzd7apt0sze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwehs1f29lvzd7apt0sze.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compound Effect of Doing Things Right
&lt;/h2&gt;

&lt;p&gt;When you fix the small stuff immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You don't accumulate exceptions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of "this works everywhere except X, Y, and Z" - it just works everywhere. No special cases to remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your codebase stays navigable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;6 months from now, I won't be reverse-engineering my own decisions. The code says what it does because I had time to make it clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bugs stay bugs - not symptoms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When something breaks, it's actually broken. Not a cascade from some workaround three layers deep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Onboarding stays fast.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;New contributors (or future me) don't need a tour guide through the historical accidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Day 0 Matters
&lt;/h2&gt;

&lt;p&gt;The best time to do things right is when you're writing the code. The context is loaded. The tradeoffs are clear. The cost is minimal.&lt;/p&gt;

&lt;p&gt;Every day you wait, the cost goes up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More code depends on the bad pattern&lt;/li&gt;
&lt;li&gt;More people learn the workaround&lt;/li&gt;
&lt;li&gt;More exceptions get documented&lt;/li&gt;
&lt;li&gt;More tests encode the wrong behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI doesn't just help you ship faster. It gives you back the time to ship &lt;em&gt;correctly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you're starting something new in 2025, use AI from day one. Not to cut corners - to finally have time to care about the corners.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Small Stuff I've Actually Done
&lt;/h2&gt;

&lt;p&gt;Things that would have been "later" at a normal startup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent loading states across every component&lt;/li&gt;
&lt;li&gt;Proper error boundaries with helpful messages&lt;/li&gt;
&lt;li&gt;Animations that feel intentional, not jarring&lt;/li&gt;
&lt;li&gt;Forms that remember your progress&lt;/li&gt;
&lt;li&gt;One way to do each thing (not three legacy approaches)&lt;/li&gt;
&lt;li&gt;Comments that explain &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;what&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Tests that run in seconds, not minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are impressive individually. Together, they're the difference between software that feels maintained and software that feels abandoned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real AI Advantage
&lt;/h2&gt;

&lt;p&gt;Everyone talks about AI making you 10x faster.&lt;/p&gt;

&lt;p&gt;I think the real advantage is simpler: &lt;strong&gt;AI makes "do it right" and "do it now" the same thing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't have to choose between shipping and quality. You don't have to accrue debt you'll never pay. You don't have to explain to your future self why the code is the way it is.&lt;/p&gt;

&lt;p&gt;You just build it properly the first time, because you finally can.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What "later" tasks are piling up in your codebase? What would you fix if you had the time?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codequality</category>
      <category>productivity</category>
      <category>startup</category>
    </item>
    <item>
      <title>Marketing as Code: Why My Brand Docs Live in Git</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Fri, 16 Jan 2026 01:38:28 +0000</pubDate>
      <link>https://forem.com/traviticus/marketing-as-code-why-my-brand-docs-live-in-git-4efd</link>
      <guid>https://forem.com/traviticus/marketing-as-code-why-my-brand-docs-live-in-git-4efd</guid>
      <description>&lt;p&gt;Developers are told to "build in public" and "do marketing."&lt;/p&gt;

&lt;p&gt;But we're also told to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notion for brand docs&lt;/li&gt;
&lt;li&gt;Google Docs for content drafts&lt;/li&gt;
&lt;li&gt;Spreadsheets for tracking&lt;/li&gt;
&lt;li&gt;Buffer for scheduling&lt;/li&gt;
&lt;li&gt;5 dashboards for analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;Here's a different approach: &lt;strong&gt;put it all in your codebase.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/docs/marketing/
├── brand-voice.md           # Who we are, how we talk
├── daily-checklist.md       # Today's priorities
├── competitive-analysis.md  # Positioning
└── social/
    ├── CLAUDE.md            # Guide for AI agents
    ├── devto/
    │   ├── activity-log.md  # Posts, comments, engagement
    │   └── posts/           # Drafts and published content
    ├── ih/
    │   ├── activity-log.md
    │   └── posts/
    ├── x/
    │   └── activity-log.md
    └── hn/
        └── activity-log.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Skill
&lt;/h2&gt;

&lt;p&gt;I built a Claude Code skill that loads all of this automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# marketing-persona skill&lt;/span&gt;

&lt;span class="gu"&gt;## Session Startup&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Read /docs/marketing/brand-voice.md
&lt;span class="p"&gt;2.&lt;/span&gt; Check /docs/marketing/daily-checklist.md
&lt;span class="p"&gt;3.&lt;/span&gt; Search for relevant articles to comment on
&lt;span class="p"&gt;4.&lt;/span&gt; Present today's agenda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I invoke &lt;code&gt;/marketing-persona&lt;/code&gt;, Claude has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full brand context&lt;/li&gt;
&lt;li&gt;What I did yesterday&lt;/li&gt;
&lt;li&gt;What worked (engagement numbers)&lt;/li&gt;
&lt;li&gt;What's on today's list&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Single source of truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No "it was in the Google Doc" vs "I thought it was in Notion." Everything is in &lt;code&gt;/docs/marketing/&lt;/code&gt;. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI has full context.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude reads the activity logs. Knows what I posted last week. Knows which comments got replies. Suggests what to do next based on what's actually working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git history = accountability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can see exactly when I changed positioning. What comments I left last week. When I last updated the brand voice. It's all versioned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Searchable.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"user123"&lt;/span&gt; docs/marketing/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every interaction with that person, instantly. Try that with Slack threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New session = instant onboarding.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A new Claude session reads the docs and has full context in seconds. No "let me catch you up on what we've been doing."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something to do during long tasks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code handles big refactors and complex features that can take 10-20 minutes to run. Instead of context-switching to Twitter or refreshing my inbox, I draft and edit marketing posts. The AI writes drafts, I refine them, and by the time we're done, the refactor is complete. Productive waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;50 minute session today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 comments on relevant posts (Claude found them via web search)&lt;/li&gt;
&lt;li&gt;Activity logs updated&lt;/li&gt;
&lt;li&gt;Tomorrow's checklist ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not a marketer. But with the right docs + AI context, I don't need to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This is part of a pattern I'm seeing: &lt;strong&gt;docs as the API between humans and AI agents.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/docs/marketing&lt;/code&gt; → AI handles content, campaigns, tracking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/docs/architecture&lt;/code&gt; → AI understands system to ship features&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/docs/support&lt;/code&gt; → AI answers questions with full product context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The better your docs, the more your AI can do autonomously.&lt;/p&gt;




&lt;p&gt;Anyone else keeping business docs in their codebase? I'm curious what other "non-code" things people are version controlling.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>git</category>
      <category>marketing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Status Field That Grew Three Heads (And How We Fixed It)</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Thu, 15 Jan 2026 02:16:54 +0000</pubDate>
      <link>https://forem.com/traviticus/the-status-field-that-grew-three-heads-and-how-we-fixed-it-27i3</link>
      <guid>https://forem.com/traviticus/the-status-field-that-grew-three-heads-and-how-we-fixed-it-27i3</guid>
      <description>&lt;p&gt;I shipped a "simple" status field six months ago.&lt;/p&gt;

&lt;p&gt;Yesterday I deleted it and replaced it with... a status field.&lt;/p&gt;

&lt;p&gt;Here's how three fields became one, and why it took 193 files to fix a problem I created.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Sin
&lt;/h2&gt;

&lt;p&gt;When I first built import/export configs, the status was obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ImportConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"status"`&lt;/span&gt; &lt;span class="c"&gt;// "draft" | "active" | "paused"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Simple. Done.&lt;/p&gt;

&lt;p&gt;Then users started running imports. I needed to know if the last run succeeded or failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ImportConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"status"`&lt;/span&gt;        &lt;span class="c"&gt;// "draft" | "active" | "paused"&lt;/span&gt;
    &lt;span class="n"&gt;LastRunStatus&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"lastRunStatus"`&lt;/span&gt; &lt;span class="c"&gt;// "success" | "failed" | "running"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still manageable. Two fields, two concerns.&lt;/p&gt;

&lt;p&gt;Then I built the wizard. Users needed to save progress mid-flow without creating broken configs. Easy fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ImportConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"status"`&lt;/span&gt;
    &lt;span class="n"&gt;LastRunStatus&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"lastRunStatus"`&lt;/span&gt;
    &lt;span class="n"&gt;IsDraft&lt;/span&gt;       &lt;span class="kt"&gt;bool&lt;/span&gt;   &lt;span class="s"&gt;`json:"isDraft"`&lt;/span&gt; &lt;span class="c"&gt;// wizard in progress&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three heads. One monster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Combinatorial Nightmare
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9xmufytz8q2r5vlykay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9xmufytz8q2r5vlykay.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the problem with three fields: they create a state matrix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: draft | active | paused | disabled | completed
LastRunStatus: success | failed | running | (empty)
IsDraft: true | false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;strong&gt;5 × 4 × 2 = 40 possible states&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most of them were nonsense. What does &lt;code&gt;status=active, lastRunStatus=running, isDraft=true&lt;/code&gt; mean?&lt;/p&gt;

&lt;p&gt;I had no idea. Neither did my code.&lt;/p&gt;

&lt;p&gt;The frontend had this gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDisplayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImportConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDraft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastRunStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastRunStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ¯\_(ツ)_/¯&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That function was wrong. I just didn't know which cases were wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recognition Moment
&lt;/h2&gt;

&lt;p&gt;I was adding a new feature when I realized I couldn't answer a basic question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Is this import ready to run?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer required checking three fields, understanding their precedence, and hoping I got the logic right.&lt;/p&gt;

&lt;p&gt;That's when I knew: I hadn't modeled status. I'd accumulated symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: One Field to Rule Them All
&lt;/h2&gt;

&lt;p&gt;The refactor was simple in concept:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; 3 fields tracking overlapping concerns&lt;br&gt;
&lt;strong&gt;After:&lt;/strong&gt; 1 field with 5 mutually exclusive states&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StatusDraft&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"draft"&lt;/span&gt;   &lt;span class="c"&gt;// Wizard incomplete, missing required fields&lt;/span&gt;
    &lt;span class="n"&gt;StatusReady&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ready"&lt;/span&gt;   &lt;span class="c"&gt;// Complete config, can be run&lt;/span&gt;
    &lt;span class="n"&gt;StatusRunning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"running"&lt;/span&gt; &lt;span class="c"&gt;// Currently executing&lt;/span&gt;
    &lt;span class="n"&gt;StatusPaused&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"paused"&lt;/span&gt;  &lt;span class="c"&gt;// User paused execution&lt;/span&gt;
    &lt;span class="n"&gt;StatusFailed&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"failed"&lt;/span&gt;  &lt;span class="c"&gt;// Last run failed, needs attention&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ImportConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"status"`&lt;/span&gt; &lt;span class="c"&gt;// One of the above. That's it.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No matrix. No precedence. No guessing.&lt;/p&gt;

&lt;p&gt;"Is this import ready to run?" → &lt;code&gt;config.Status == StatusReady&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  But Wait, What About the Wizard?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;isDraft&lt;/code&gt; field existed because wizards need to save partial progress.&lt;/p&gt;

&lt;p&gt;Removing it meant solving a different problem: &lt;strong&gt;where does wizard state live?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Answer: in a &lt;code&gt;DraftData&lt;/code&gt; field that only exists when &lt;code&gt;Status == StatusDraft&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ImportConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;     &lt;span class="s"&gt;`json:"status"`&lt;/span&gt;
    &lt;span class="n"&gt;DraftData&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DraftData&lt;/span&gt; &lt;span class="s"&gt;`json:"draftData,omitempty"`&lt;/span&gt; &lt;span class="c"&gt;// nil unless Status=draft&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DraftData&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CurrentStep&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"currentStep"`&lt;/span&gt;
    &lt;span class="n"&gt;PendingSchema&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PendingSchema&lt;/span&gt;  &lt;span class="s"&gt;`json:"pendingSchema,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;PendingDataType&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PendingDataType&lt;/span&gt; &lt;span class="s"&gt;`json:"pendingDataType,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;pending resources live in draft data, not in real tables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you're mid-wizard creating a schema, that schema doesn't exist yet. It's a &lt;em&gt;pending&lt;/em&gt; schema stored in DraftData. Only when you finalize does it become real.&lt;/p&gt;

&lt;p&gt;No more orphaned schemas from abandoned wizards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Damage Report
&lt;/h2&gt;

&lt;p&gt;Fixing this touched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;193 files&lt;/li&gt;
&lt;li&gt;11,786 additions&lt;/li&gt;
&lt;li&gt;4,179 deletions&lt;/li&gt;
&lt;li&gt;Backend models, services, handlers&lt;/li&gt;
&lt;li&gt;Frontend types, hooks, wizards&lt;/li&gt;
&lt;li&gt;Tests across both&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Was it worth it?&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;getDisplayStatus&lt;/code&gt; function is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDisplayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImportConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes. It was worth it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. State fields multiply.&lt;/strong&gt;&lt;br&gt;
One becomes two becomes three. Each addition feels small. The complexity is combinatorial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "What state is this?" should be trivial to answer.&lt;/strong&gt;&lt;br&gt;
If you need a flowchart, you have a modeling problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pending resources aren't real resources.&lt;/strong&gt;&lt;br&gt;
Wizard state is draft data, not partially-created entities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Big refactors are just many small changes.&lt;/strong&gt;&lt;br&gt;
193 files sounds scary. It was really "update Status constant" × 193.&lt;/p&gt;




&lt;p&gt;Building &lt;a href="https://www.flywheeletl.io" rel="noopener noreferrer"&gt;Flywheel&lt;/a&gt; - data pipelines for startups. If you've ever watched a "simple" field grow three heads, I'd love to hear your war stories.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>codequality</category>
      <category>devjournal</category>
      <category>go</category>
    </item>
    <item>
      <title>We Deleted 5,600 Lines of Code with Claude (and Found 1 Bug)</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Wed, 14 Jan 2026 01:14:02 +0000</pubDate>
      <link>https://forem.com/traviticus/we-deleted-5600-lines-of-code-with-claude-and-found-1-bug-on9</link>
      <guid>https://forem.com/traviticus/we-deleted-5600-lines-of-code-with-claude-and-found-1-bug-on9</guid>
      <description>&lt;p&gt;I spent weeks building a provider registry system for my data pipeline platform. Sources, connection templates, database seeds, UUID mappings, capability checks, service arrays.&lt;/p&gt;

&lt;p&gt;Then I deleted almost all of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;194 files changed, 8,798 insertions(+), 14,411 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Net result: ~5,600 lines of code removed. One bug found after the full rewrite.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Over-Engineered System I Built
&lt;/h2&gt;

&lt;p&gt;I needed to manage providers (Google Cloud, AWS, Postgres) and their services (BigQuery, Firestore, DynamoDB). Seemed simple enough.&lt;/p&gt;

&lt;p&gt;But I built this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;What It Stored&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seeds/sources_seed.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source entities with services arrays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seeds/templates_seed.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConnectionTemplate entities with schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seeds/constants.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded UUIDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend/components/connections/index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UUID → Component mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend/components/connections/*.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded &lt;code&gt;selectedServices&lt;/code&gt; arrays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frontend/lib/services/google-cloud-capabilities.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OAuth scopes per service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database (Firestore)&lt;/td&gt;
&lt;td&gt;Sources and ConnectionTemplates collections&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;8+ files defining the same information in different ways.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Want to add a new provider? Touch 8 files. Want to add a service to an existing provider? 6 files. Want to understand how it all fits together? Good luck.&lt;/p&gt;

&lt;p&gt;I told myself this was "flexible" and "extensible."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Realization
&lt;/h2&gt;

&lt;p&gt;Then I actually thought about it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing was dynamic.&lt;/strong&gt; OAuth scopes are locked at the OAuth app level. You can't grant different scopes per connection - they're baked into the OAuth consent screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was no use case for partial service access.&lt;/strong&gt; "This Google Cloud connection has BigQuery but not Firestore" - when would that ever happen? A connection is just credentials. If those credentials can't access BigQuery, the API returns 401. Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I was maintaining complexity for flexibility I'd never use.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The "sophisticated" architecture was solving a problem that didn't exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Replacement: 20 Lines
&lt;/h2&gt;

&lt;p&gt;I had no users yet. No backward compatibility concerns. No data to migrate.&lt;/p&gt;

&lt;p&gt;So instead of refactoring, I asked: &lt;em&gt;What if I just deleted everything and replaced it with a single config file?&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend/lib/providers.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROVIDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google-cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google Cloud Platform&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bigquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gcs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pubsub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Amazon Web Services&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PostgreSQL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProviderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;PROVIDERS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. ~20 lines replacing database tables, seeds, UUIDs, capability files, and registries.&lt;/p&gt;

&lt;p&gt;The connection model went from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: 6 fields, UUID lookups, redundant data&lt;/span&gt;
&lt;span class="nx"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c7b3d8e9-5f2a-4b1c-9d6e-8a3b5c7d9e1f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// UUID lookup&lt;/span&gt;
  &lt;span class="na"&gt;templateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;template-google-cloud-oauth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Another UUID&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bigquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                &lt;span class="c1"&gt;// Redundant&lt;/span&gt;
  &lt;span class="na"&gt;connectionConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;selectedServices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bigquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;// Duplicate&lt;/span&gt;
    &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: 4 fields, string ID, no redundancy&lt;/span&gt;
&lt;span class="nx"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google-cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Just the key from PROVIDERS&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// Provider-specific config&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How Claude Made This Possible
&lt;/h2&gt;

&lt;p&gt;This wasn't "let Claude write some code." This was &lt;strong&gt;AI-assisted architecture surgery.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Mapping the Blast Radius
&lt;/h3&gt;

&lt;p&gt;I gave Claude the codebase context and asked it to find every file that referenced the old system. It identified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All imports of the old types&lt;/li&gt;
&lt;li&gt;All usages of the UUID constants&lt;/li&gt;
&lt;li&gt;Frontend components using &lt;code&gt;sourceId&lt;/code&gt; or &lt;code&gt;templateId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test files that would need updates&lt;/li&gt;
&lt;li&gt;The order of operations to avoid breaking intermediate states&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Systematic Execution
&lt;/h3&gt;

&lt;p&gt;194 files is a lot to change by hand. More importantly, it's a lot to change &lt;em&gt;correctly&lt;/em&gt;. Claude worked through them methodically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend (Go):
- connections/model.go    → Remove Services, TemplateId; add ProviderId
- connections/service.go  → Update creation/validation logic
- connections/handler.go  → Update API request/response
- connections/dao.go      → Update Firestore queries
- router.go               → Remove /v1/sources/* routes
- Delete entire sources/ package (9 files)
- Delete entire connectiontemplate/ package (10 files)
- Delete seeds/sources_seed.go, templates_seed.go

Frontend (TypeScript):
- lib/providers.ts        → New static config (the 20 lines)
- hooks/use-source.ts     → Deleted
- hooks/use-connection-template.ts → Deleted
- services/source-service.ts → Deleted
- services/google-cloud-capabilities.ts → Deleted
- Update 40+ component files using the old types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Updating Tests in Lockstep
&lt;/h3&gt;

&lt;p&gt;This is the key: &lt;strong&gt;we didn't delete tests, we updated them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every service change came with corresponding test updates. When we deleted the &lt;code&gt;sources/&lt;/code&gt; package, we also deleted its tests. When we simplified the connection model, we updated the connection tests.&lt;/p&gt;

&lt;p&gt;The test suite stayed green throughout the refactor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Only 1 Bug?
&lt;/h2&gt;

&lt;p&gt;After changing 194 files, we found exactly &lt;strong&gt;one bug&lt;/strong&gt; in end-to-end testing.&lt;/p&gt;

&lt;p&gt;That's not luck. That's what happens when:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You have comprehensive test coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests catch regressions immediately. When I changed the connection model, tests told me exactly which handlers and services needed updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You refactor with tests, not after.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every change included its test updates in the same commit. Tests weren't an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. AI helps you be systematic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude doesn't forget to update a file in some distant corner of the codebase. It doesn't get tired after file 80 and start making mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. You have a clear architectural vision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wrote a detailed plan document before touching code. The target state was unambiguous: one config file, string-based provider IDs, no services arrays on connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complexity is a choice
&lt;/h3&gt;

&lt;p&gt;I built the complex system. Nobody forced me to create UUID-based lookups and database seeds for static data. I did that because it felt "proper."&lt;/p&gt;

&lt;p&gt;Sometimes the proper solution is a 20-line config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI is best for architecture, not just autocomplete
&lt;/h3&gt;

&lt;p&gt;The value wasn't "Claude wrote code faster." The value was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude helped identify all the tentacles of the old system&lt;/li&gt;
&lt;li&gt;Claude maintained context across 194 files&lt;/li&gt;
&lt;li&gt;Claude was systematic where I would have gotten tired&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Well-tested code enables fearless deletion
&lt;/h3&gt;

&lt;p&gt;I could mass-delete code because I trusted my tests. Every deletion was validated. No "I think this is safe" - either tests passed or they didn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  No users = no excuses
&lt;/h3&gt;

&lt;p&gt;Having no users yet meant I had no excuse to keep complexity around. No backward compatibility. No migration scripts. Just delete and move on.&lt;/p&gt;

&lt;p&gt;If you're early stage and carrying technical debt, now is the cheapest time to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Developer Experience
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Adding a new provider:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add entry to &lt;code&gt;PROVIDERS&lt;/code&gt; config (1 line)&lt;/li&gt;
&lt;li&gt;Create connection form component&lt;/li&gt;
&lt;li&gt;Create service config components&lt;/li&gt;
&lt;li&gt;Create backend handlers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Adding a service to existing provider:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add to &lt;code&gt;PROVIDERS[providerId].services&lt;/code&gt; array (1 line)&lt;/li&gt;
&lt;li&gt;Create service config components&lt;/li&gt;
&lt;li&gt;Create backend handlers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No seeds. No migrations. No UUIDs. No template entities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you're staring at a system that feels heavier than it needs to be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask what problem it's solving.&lt;/strong&gt; Is that problem real or hypothetical?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check if anything is actually dynamic.&lt;/strong&gt; If the "flexible" parts never flex, they're just complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you have good tests, trust them.&lt;/strong&gt; They'll catch your mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI to map the blast radius.&lt;/strong&gt; It's better at finding all the references than you are.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes the answer is mass deletion.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you ever deleted a system you built because you realized it was over-engineered? What helped you make that call?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>codequality</category>
      <category>go</category>
      <category>llm</category>
    </item>
    <item>
      <title>I Built a Full AWS S3 Integration in Under 2 Hours—From First Prompt to Production</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Mon, 12 Jan 2026 20:39:57 +0000</pubDate>
      <link>https://forem.com/traviticus/i-built-a-full-aws-s3-integration-in-under-2-hours-from-first-prompt-to-production-4djb</link>
      <guid>https://forem.com/traviticus/i-built-a-full-aws-s3-integration-in-under-2-hours-from-first-prompt-to-production-4djb</guid>
      <description>&lt;p&gt;Recently I added complete S3 support to &lt;a href="https://flywheeletl.io" rel="noopener noreferrer"&gt;Flywheel&lt;/a&gt;—import and export, multiple file formats, chunked processing for large datasets. From first prompt to deployed in production: &lt;strong&gt;1 hour 51 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because I'm fast. Because my codebase is designed for AI to extend it.&lt;/p&gt;

&lt;p&gt;Here's exactly what I typed and what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Flywheel already had AWS support for DynamoDB. I wanted to add S3. Same provider, new service. In a traditional codebase, this would mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remembering how DynamoDB was implemented&lt;/li&gt;
&lt;li&gt;Finding all the files that need to change&lt;/li&gt;
&lt;li&gt;Copy-pasting and modifying boilerplate&lt;/li&gt;
&lt;li&gt;Hoping I didn't miss a registration somewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, I have a &lt;code&gt;/provider-setup&lt;/code&gt; skill—a structured prompt that knows our patterns, templates, and conventions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prompts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prompt 1: Kick it off
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using /provider-setup lets plan out the work for reading from/to S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude read the skill file, checked our existing AWS patterns (DynamoDB), and asked two clarifying questions: file formats and export mode. I answered "JSON + CSV, batch files." It created a checklist and showed me the implementation plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 2: Ship it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lets go for it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four words. Over the next ~25 minutes, Claude generated the full stack—Go backend (handlers, services, event handlers), TypeScript frontend (config forms, bucket selection), plus all the wiring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10:31 PM&lt;/strong&gt; — Preview endpoint working, listing S3 files in the UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10:34 PM&lt;/strong&gt; — Full import complete, 12 records pulled from S3&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt 3: Bug fix
&lt;/h3&gt;

&lt;p&gt;Export failed. I pasted the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PermanentRedirect: The bucket you are attempting to access
must be addressed using the specified endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude identified it immediately: S3's &lt;code&gt;ListBuckets&lt;/code&gt; is global, but writes need regional endpoints. Fix: call &lt;code&gt;GetBucketLocation&lt;/code&gt; per bucket, auto-select region. &lt;strong&gt;10:40 PM&lt;/strong&gt; — Export working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompts 4-5: Refinements
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If we already get the region from the bucket list why even show the region selector?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;does buckets even need the region query param?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude agreed with both. Removed the redundant UI, made the API param optional. Simpler UX, cleaner API.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10:00 PM&lt;/td&gt;
&lt;td&gt;Started&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:05 PM&lt;/td&gt;
&lt;td&gt;Requirements gathered, plan approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:31 PM&lt;/td&gt;
&lt;td&gt;Preview endpoint working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:34 PM&lt;/td&gt;
&lt;td&gt;Full import complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:40 PM&lt;/td&gt;
&lt;td&gt;Export complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:45 PM&lt;/td&gt;
&lt;td&gt;Refinements done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11:44 PM&lt;/td&gt;
&lt;td&gt;Passed &lt;code&gt;/check&lt;/code&gt;, CI pipeline running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11:51 PM&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Deployed to production&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total prompts:&lt;/strong&gt; 5&lt;br&gt;
&lt;strong&gt;S3 implementation:&lt;/strong&gt; 45 minutes&lt;br&gt;
&lt;strong&gt;Time to production:&lt;/strong&gt; 1 hour 51 minutes (included fixing an unrelated issue from a previous release)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4up7it6e17z101q6pah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4up7it6e17z101q6pah.png" alt="S3 flow graph showing import from S3, Contacts data type, and exports to S3" width="800" height="286"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished S3 integration: import → transform → export, all at 100% success rate&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;This isn't about AI being magic. It's about the codebase being &lt;em&gt;ready&lt;/em&gt; for AI.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Ruthless consistency
&lt;/h3&gt;

&lt;p&gt;Every provider follows the same structure. Same files, same patterns, same naming conventions. Claude learned DynamoDB once—S3 was just "do that again for a different service."&lt;/p&gt;

&lt;p&gt;For example, every provider package in our Go backend has the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backend/main/{provider}/
├── model.go           # Config structs, validation
├── service.go         # Business logic, import/export handling
├── handler.go         # HTTP endpoints
├── chunk_fetcher.go   # Pagination for imports
├── export_writer.go   # Batch writing for exports
├── subscriptions.go   # Event handlers
└── helpers.go         # Client creation, utilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Claude sees "add S3 support," it knows exactly what files to create and where they go. No guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CLAUDE.md as the single source of truth
&lt;/h3&gt;

&lt;p&gt;At the root of the repo, there's a &lt;code&gt;CLAUDE.md&lt;/code&gt; file (~300 lines) that explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project architecture and conventions&lt;/li&gt;
&lt;li&gt;Common patterns (React Query keys, toast notifications, state management)&lt;/li&gt;
&lt;li&gt;Where to find detailed docs for specific domains&lt;/li&gt;
&lt;li&gt;Critical rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Core Principles&lt;/span&gt;

&lt;span class="gu"&gt;### Architecture Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Edge-to-Service Pattern**&lt;/span&gt;: Handlers and subscriptions MUST delegate to service layer
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Dual Records**&lt;/span&gt;: External records from sources + canonical normalized records
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**EBAC Authorization**&lt;/span&gt;: Entity-based access control with role and entity-specific permissions
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Event Constants**&lt;/span&gt;: Always use pubsub package constants, never strings

&lt;span class="gu"&gt;### React Query Keys&lt;/span&gt;
['organizations']                    // List
['organizations', orgId]             // Single
['organizations', orgId, 'members']  // Related
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't documentation for humans—it's context for AI. Claude reads it at the start of every session and immediately knows how we do things.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Skills for repeated workflows
&lt;/h3&gt;

&lt;p&gt;Skills are markdown files in &lt;code&gt;.claude/skills/&lt;/code&gt; that encode multi-step workflows. The &lt;code&gt;/provider-setup&lt;/code&gt; skill is about 500 lines and includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A decision tree for gathering requirements&lt;/li&gt;
&lt;li&gt;Patterns for different provider types (databases vs. object storage vs. message queues)&lt;/li&gt;
&lt;li&gt;Checklists of all files that need to be created&lt;/li&gt;
&lt;li&gt;Templates showing our conventions&lt;/li&gt;
&lt;li&gt;References to existing implementations for context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the workflow section looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Workflow&lt;/span&gt;

&lt;span class="gu"&gt;### Step 1: Gather Requirements&lt;/span&gt;
Ask the user to clarify:
&lt;span class="p"&gt;-&lt;/span&gt; Provider name (e.g., "mongodb", "s3")
&lt;span class="p"&gt;-&lt;/span&gt; Capabilities: Import only, Export only, or Both
&lt;span class="p"&gt;-&lt;/span&gt; Connection credentials needed
&lt;span class="p"&gt;-&lt;/span&gt; Data access pattern (table-based, file-based, query-based)

&lt;span class="gu"&gt;### Step 2: Check Existing Patterns&lt;/span&gt;
If adding to an existing provider (e.g., S3 to AWS):
&lt;span class="p"&gt;-&lt;/span&gt; Check for shared patterns (auth, region selection)
&lt;span class="p"&gt;-&lt;/span&gt; Reuse existing components, don't duplicate

&lt;span class="gu"&gt;### Step 3: Initialize Checklist&lt;/span&gt;
[... creates a todo list of all required files ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm constantly iterating on this skill—every integration teaches me something new to add. But now every new integration starts from the same playbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Small, focused prompts work
&lt;/h3&gt;

&lt;p&gt;Notice I didn't write detailed specifications. I said "lets go for it" and let Claude execute the plan it had already created. When there was a bug, I described what I observed, not what I thought the fix should be.&lt;/p&gt;

&lt;p&gt;The codebase has enough structure that Claude can fill in the gaps correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A &lt;code&gt;/check&lt;/code&gt; skill for validation
&lt;/h3&gt;

&lt;p&gt;Before any commit, I run &lt;code&gt;/check --run-all&lt;/code&gt;. This skill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs linting and type checks&lt;/li&gt;
&lt;li&gt;Runs tests&lt;/li&gt;
&lt;li&gt;Checks if documentation needs updating&lt;/li&gt;
&lt;li&gt;Flags when marketing content should be created (like "you just shipped a feature, maybe write about it")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One command catches issues before CI. It's not just about code quality—it's about making the whole workflow predictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell Other Devs
&lt;/h2&gt;

&lt;p&gt;If you're using AI coding tools daily, your codebase is now a prompt. You can fight that or optimize for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consistent file structure&lt;/strong&gt; - Same files in the same places&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CLAUDE.md&lt;/strong&gt; - Or whatever your AI tool uses for context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document conventions explicitly&lt;/strong&gt; - Don't rely on tribal knowledge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create skills for repeated work&lt;/strong&gt; - This is the multiplier. A &lt;code&gt;/provider-setup&lt;/code&gt; skill means every new integration starts from the same playbook. A &lt;code&gt;/check&lt;/code&gt; skill means code quality is one command. The upfront investment pays back every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 40-minute S3 integration wasn't a one-time trick. It's how we work now. GCS took about the same. Firestore was similar. The pattern compounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try This Yourself
&lt;/h2&gt;

&lt;p&gt;Next time you're about to build something repetitive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Document the pattern you're following&lt;/li&gt;
&lt;li&gt;Create a checklist of all the files/registrations needed&lt;/li&gt;
&lt;li&gt;Put it in a skill file or prompt template&lt;/li&gt;
&lt;li&gt;Let AI execute while you review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first time takes longer. Every subsequent time is under 2 hours to production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building &lt;a href="https://flywheeletl.io" rel="noopener noreferrer"&gt;Flywheel&lt;/a&gt; in public. Follow along on &lt;a href="https://x.com/travis_flywheel" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; or &lt;a href="https://indiehackers.com/Traviticus" rel="noopener noreferrer"&gt;Indie Hackers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How strong fundamentals + AI helped me build a data pipeline platform in 6 months (solo)</title>
      <dc:creator>Travis Wilson</dc:creator>
      <pubDate>Sun, 11 Jan 2026 04:38:46 +0000</pubDate>
      <link>https://forem.com/traviticus/how-strong-fundamentals-ai-helped-me-build-a-data-pipeline-platform-in-6-months-solo-1g4b</link>
      <guid>https://forem.com/traviticus/how-strong-fundamentals-ai-helped-me-build-a-data-pipeline-platform-in-6-months-solo-1g4b</guid>
      <description>&lt;p&gt;I'm Travis, a staff engineer with 12+ years building data pipelines at various companies. Six months ago, I started building Flywheel - a data pipeline platform for startups. I'm a solo founder, working evenings and weekends.&lt;/p&gt;

&lt;p&gt;Progress has been faster than expected. Not because of some secret productivity hack, but because of a combination I didn't expect: boring software fundamentals paired with AI-assisted development (specifically Claude from Anthropic).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Fundamentals Matter More With AI
&lt;/h2&gt;

&lt;p&gt;When I started, I made a deliberate choice to invest in foundations before features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code (Terraform)&lt;/strong&gt; - Every piece of GCP infrastructure is codified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-Driven Architecture&lt;/strong&gt; - Pub/Sub for all async operations, clean separation of concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Layer Pattern&lt;/strong&gt; - Handlers delegate to services, business logic is isolated and testable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent Conventions&lt;/strong&gt; - Every domain follows the same structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This felt slow at first. But here's the thing: &lt;strong&gt;AI tools like Claude are force multipliers for clean codebases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When your architecture is consistent, Claude can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate new endpoints that follow your existing patterns&lt;/li&gt;
&lt;li&gt;Write tests that match your testing conventions&lt;/li&gt;
&lt;li&gt;Refactor safely because tests catch regressions&lt;/li&gt;
&lt;li&gt;Understand context faster because the code is organized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a messy codebase, AI suggestions are often wrong or inconsistent. In a clean codebase, they're usually right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Flywheel is a data pipeline platform designed for early-stage startups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect sources (databases, APIs, files)&lt;/li&gt;
&lt;li&gt;Transform and normalize data&lt;/li&gt;
&lt;li&gt;Export to warehouses (BigQuery, PostgreSQL, Domo, etc.)&lt;/li&gt;
&lt;li&gt;Real-time monitoring and scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's the kind of infrastructure that typically takes a team 12-18 months. I built the core in 6 months, solo, while working a full-time job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current stats:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4,366 tests&lt;/strong&gt; (2,471 backend + 1,895 frontend)&lt;/li&gt;
&lt;li&gt;Test suite runs in under a minute&lt;/li&gt;
&lt;li&gt;Merge to deployed: ~7 minutes with full build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of the unit tests, I've built a solid end-to-end test suite for the backend that exercises the full system. Now I'm working with Claude Code's built-in Playwright agent to build out a frontend end-to-end suite. The goal: release quickly &lt;em&gt;and&lt;/em&gt; confidently. Tests aren't just about catching bugs - they're what let me ship fast without second-guessing everything.&lt;/p&gt;

&lt;p&gt;I also lean heavily on Claude Code's built-in agents like code-explorer (for understanding unfamiliar parts of the codebase), code-reviewer (catches issues before I commit), and code-architect (helps plan features that fit existing patterns). These aren't magic - they work because the codebase is consistent enough for them to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Claude Workflow (What It Actually Looks Like)
&lt;/h2&gt;

&lt;p&gt;I'm not using AI to replace thinking - I'm using it to break down complexity and maintain velocity.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Decompose the idea
&lt;/h3&gt;

&lt;p&gt;I start with a high-level feature idea - like a visual flow graph for pipelines - and work with Claude to break it into manageable chunks.&lt;/p&gt;

&lt;p&gt;Real example: A friend's company needed to sync CSV files from S3 to Domo, with Flywheel handling the column name → ordinal mapping. Sounds simple, but it required building out: S3 source support, Domo destination support, CSV parsing, and ordinal column handling.&lt;/p&gt;

&lt;p&gt;I worked with Claude to decompose this into a plan with clear chunks. Each chunk becomes its own focused task.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Right-size the work
&lt;/h3&gt;

&lt;p&gt;Here's something I learned the hard way: &lt;strong&gt;context windows matter.&lt;/strong&gt; Go outside them and things get messy.&lt;/p&gt;

&lt;p&gt;I use my experience to prioritize what to build first, then have a fresh agent break each chunk into what fits well in one context window. This keeps Claude focused and prevents the drift you get in long sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Manual test, then write tests
&lt;/h3&gt;

&lt;p&gt;I test manually first because I want to iterate quickly. Once I'm happy with how something works, &lt;em&gt;then&lt;/em&gt; I have Claude write the tests to lock it in.&lt;/p&gt;

&lt;p&gt;I also built a &lt;code&gt;/check&lt;/code&gt; command - scripts that verify everything: tests passing, docs up to date, linting clean. It's my safety net between chunks.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Simplify at the end
&lt;/h3&gt;

&lt;p&gt;Once a feature is complete and tests are green, I run a code-simplifier pass. Because we've built up tests along the way, refactoring is safe. This is where the clean codebase pays off - Claude can refactor confidently.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Refactor continuously
&lt;/h3&gt;

&lt;p&gt;Every new feature that touches existing code is an opportunity to clean up. Small refactors. Better names. Clearer abstractions. It feels slow, but it's what keeps the codebase "AI-friendly" over time. Lazy shortcuts compound into a mess that even AI can't help you with.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Bugs happen (here's how I handle critical ones)
&lt;/h3&gt;

&lt;p&gt;Do I commit bugs? Absolutely. But when a critical bug escapes that never should have - I don't let Claude just fix it. I make it write a test that reproduces the bug first. Then I verify the suggested fix manually.&lt;/p&gt;

&lt;p&gt;This turns every escaped bug into a permanent regression test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Didn't Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Letting AI make architectural decisions&lt;/strong&gt; - Every time I let Claude "figure out" the structure, I ended up refactoring later. I design, Claude implements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long sessions without fresh context&lt;/strong&gt; - Agent drift is real. Fresh context windows keep things clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-engineering early&lt;/strong&gt; - Even with AI help, simpler is still better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Might Matter to You
&lt;/h2&gt;

&lt;p&gt;If you're building something solo (or with a tiny team), you've probably wondered whether AI tools are actually useful or just hype. My take: they're a multiplier, not a replacement. They multiply whatever you already have - good or bad.&lt;/p&gt;

&lt;p&gt;If your codebase is inconsistent, AI gives you inconsistent suggestions. If you don't know what good looks like, you can't evaluate what it produces. But if you've got experience and a clean foundation, AI lets you move at a pace that wasn't possible before.&lt;/p&gt;

&lt;p&gt;The 12+ years in software learning what works (and what doesn't) is what makes Claude useful. Not the other way around.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Invest in fundamentals before features&lt;/strong&gt; - IaC, event-driven architecture, clean patterns pay dividends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI multiplies your existing skills&lt;/strong&gt; - Experience means I know what good looks like. Claude helps me get there faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context windows are a feature, not a bug&lt;/strong&gt; - Fresh agents for each chunk keeps work focused&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests after manual verification&lt;/strong&gt; - Confirm it works, then lock it in with tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor continuously&lt;/strong&gt; - Every feature is an opportunity to clean up. This keeps AI effective long-term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs become regression tests&lt;/strong&gt; - Every escaped bug makes the system stronger&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Flywheel is in alpha - free to use while I build it out. If you're an early-stage startup that needs data pipelines without building infrastructure from scratch: &lt;a href="https://www.flywheeletl.io" rel="noopener noreferrer"&gt;flywheeletl.io&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Has anyone else found that AI tools work better (or worse) depending on code quality? I'm curious if others have experienced this "fundamentals + AI" multiplier effect.&lt;/p&gt;




</description>
      <category>go</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
