{"id":22966,"date":"2026-08-01T06:22:16","date_gmt":"2026-08-01T06:22:16","guid":{"rendered":"https:\/\/scannn.com\/the-economic-benefit-of-refactoring\/"},"modified":"2026-08-01T06:22:16","modified_gmt":"2026-08-01T06:22:16","slug":"the-economic-benefit-of-refactoring","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/the-economic-benefit-of-refactoring\/","title":{"rendered":"The Economic Benefit of Refactoring"},"content":{"rendered":"\n<div>\n<p>As part of getting to grips with the new world of agentic engineering,<br \/>\nI built an application to support my work. It\u2019s a sophisticated app:<br \/>\nhigh-quality web UI with dynamic refresh and look-up, modals and<br \/>\nauto-save, integrations to external systems, machine learning and text<br \/>\nanalysis, background jobs, and a proper environment setup with fully<br \/>\nautomated deployment. It\u2019s approximately 150,000 lines of code,<br \/>\nprimarily in Rust (~120 kLoC) with the remainder in TypeScript and<br \/>\nTerraform.<\/p>\n<p>This was entirely written by agents. Mostly Claude Code, and some use<br \/>\nof Cursor. I didn\u2019t read or review any of the code, except<br \/>\noccasionally, out of interest.<\/p>\n<p>While building the application, I could see some things going<br \/>\nawry. After watching an edit to line 4,000 of a file scroll by in the<br \/>\nterminal, I had a closer look. The data access layer had grown to over<br \/>\n6,000 lines. As more features landed, this continued to<br \/>\ngrow. Every query, read or write, repeated the same HTTP request<br \/>\nsetup, the same JSON encoding and decoding. Eventually, it reached 17,155<br \/>\nlines. In a single Rust file.<\/p>\n<h2 id=\"an-experiment-in-refactoring\">An experiment in refactoring<\/h2>\n<p>The 17,155 line file was the entire data access layer. A single,<br \/>\nself-contained module. Reviewing the code, there was no<br \/>\nde-duplication, no internal language, limited extraction of functions,<br \/>\nand very little extraction of classes. It did have a clear boundary<br \/>\nwith an interface to preserve. It was a great target for refactoring.<\/p>\n<p>The goal of refactoring an agentic code base is to spend tokens now in<br \/>\nrefactoring to make token consumption for future work lower. An<br \/>\nexperiment should be able to show that as this file was refactored the<br \/>\ntoken cost of making separate feature implementations in this code<br \/>\nbase would decrease.<\/p>\n<p>Precisely because agents never learn this was now possible to run as<br \/>\nan experiment. I could prompt a fresh agent to make exactly the same<br \/>\nchange after every refactoring stage. Unlike a human engineer, the<br \/>\nexperiment would not be tainted by learning from previous steps.<\/p>\n<ol>\n<li>Create an overall refactoring plan, following strict refactoring<br \/>\ndiscipline.<\/li>\n<li>Craft a representative change, described in a single prompt.<\/li>\n<li>Establish a baseline cost of change: in a sub-agent, execute that<br \/>\nprompt, including asking the sub-agent to report token consumption.<\/li>\n<li>Throw away the change.<\/li>\n<li>In a loop:\n<ol>\n<li>Apply a single step of the overall refactoring.<\/li>\n<li>In a sub-agent, execute <em>exactly<\/em> the same change receiving the<br \/>\ntoken cost of the change.<\/li>\n<li>Throw away the change.<\/li>\n<\/ol>\n<\/li>\n<li>Record all token costs, time to execute the change, and lines of<br \/>\ncode after each step of the refactoring, including the baseline.<\/li>\n<\/ol>\n<p>The prompt used for the representative change and the refactoring<br \/>\nsteps applied are shown in the appendices, below.<\/p>\n<p>One caveat: Claude doesn\u2019t provide reliable methods for counting<br \/>\ntokens live despite showing token counts, reporting tokens consumed<br \/>\nper session, and <strong>billing<\/strong> for tokens. I\u2019m assuming this is a<br \/>\ntemporary issue that will improve over time. Instead, the sub-agent<br \/>\nreported the number of characters received and sent and used<br \/>\n<a href=\"https:\/\/github.com\/openai\/tiktoken\">tiktoken<\/a> to approximate tokens, by dividing character count by<br \/>\nfour.<\/p>\n<h2 id=\"results\">Results<\/h2>\n<table>\n<thead>\n<tr>\n<th>Step<\/th>\n<th>Data Access Layer LoC<\/th>\n<th>Largest file LoC<\/th>\n<th>Total Rust LoC<\/th>\n<th>Input tokens per change<\/th>\n<th>Output tokens per change<\/th>\n<th>Time per change (s)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Baseline<\/td>\n<td>17,155<\/td>\n<td>17,155<\/td>\n<td>50,359<\/td>\n<td>159,564<\/td>\n<td>1,705<\/td>\n<td>342<\/td>\n<\/tr>\n<tr>\n<td>Step 1 (FirestoreClient)<\/td>\n<td>16,706<\/td>\n<td>16,706<\/td>\n<td>49,910<\/td>\n<td>155,205<\/td>\n<td>1,723<\/td>\n<td>530<\/td>\n<\/tr>\n<tr>\n<td>Step 2 (extract_doc_id, new_link)<\/td>\n<td>16,562<\/td>\n<td>16,562<\/td>\n<td>49,766<\/td>\n<td>159,227<\/td>\n<td>2,105<\/td>\n<td>574<\/td>\n<\/tr>\n<tr>\n<td>Step 3 (link-query helpers)<\/td>\n<td>16,567<\/td>\n<td>16,567<\/td>\n<td>49,771<\/td>\n<td>154,054<\/td>\n<td>2,105<\/td>\n<td>524<\/td>\n<\/tr>\n<tr>\n<td>Step 4 (FakeStore predicates)<\/td>\n<td>16,577<\/td>\n<td>16,577<\/td>\n<td>49,781<\/td>\n<td>154,146<\/td>\n<td>2,060<\/td>\n<td>654<\/td>\n<\/tr>\n<tr>\n<td>Step 5 (value ctors)<\/td>\n<td>16,469<\/td>\n<td>16,469<\/td>\n<td>49,673<\/td>\n<td>171,251<\/td>\n<td>2,036<\/td>\n<td>1,353<\/td>\n<\/tr>\n<tr>\n<td>Step 6 (FieldsBuilder)<\/td>\n<td>16,469<\/td>\n<td>16,469<\/td>\n<td>49,673<\/td>\n<td>171,251<\/td>\n<td>2,036<\/td>\n<td>1,353<\/td>\n<\/tr>\n<tr>\n<td>Step 7 (queries.rs)<\/td>\n<td>16,474<\/td>\n<td>15,670<\/td>\n<td>49,678<\/td>\n<td>151,850<\/td>\n<td>1,800<\/td>\n<td>587<\/td>\n<\/tr>\n<tr>\n<td>Step 8 (traits.rs)<\/td>\n<td>16,508<\/td>\n<td>13,845<\/td>\n<td>49,712<\/td>\n<td>132,558<\/td>\n<td>1,723<\/td>\n<td>446<\/td>\n<\/tr>\n<tr>\n<td>Step 9 (traits\/ split)<\/td>\n<td>16,508<\/td>\n<td>13,845<\/td>\n<td>49,712<\/td>\n<td>132,558<\/td>\n<td>1,723<\/td>\n<td>446<\/td>\n<\/tr>\n<tr>\n<td>Step 10 (codec.rs)<\/td>\n<td>16,521<\/td>\n<td>12,846<\/td>\n<td>49,725<\/td>\n<td>131,871<\/td>\n<td>1,750<\/td>\n<td>540<\/td>\n<\/tr>\n<tr>\n<td>Step 11 (fake_store.rs)<\/td>\n<td>16,535<\/td>\n<td>11,122<\/td>\n<td>49,739<\/td>\n<td>133,016<\/td>\n<td>2,460<\/td>\n<td>600<\/td>\n<\/tr>\n<tr>\n<td>Step 12 (store\/ split)<\/td>\n<td>16,550<\/td>\n<td>9,269<\/td>\n<td>49,754<\/td>\n<td>104,080<\/td>\n<td>2,050<\/td>\n<td>490<\/td>\n<\/tr>\n<tr>\n<td>Step 13 (co-locate tests)<\/td>\n<td>16,550<\/td>\n<td>9,269<\/td>\n<td>49,754<\/td>\n<td>104,080<\/td>\n<td>2,050<\/td>\n<td>490<\/td>\n<\/tr>\n<tr>\n<td>Step 14 (complete fake_store.rs)<\/td>\n<td>16,553<\/td>\n<td>7,225<\/td>\n<td>49,757<\/td>\n<td>107,205<\/td>\n<td>2,453<\/td>\n<td>523<\/td>\n<\/tr>\n<tr>\n<td>Step 15 (store\/ split)<\/td>\n<td>16,608<\/td>\n<td>3,695<\/td>\n<td>49,812<\/td>\n<td>27,360<\/td>\n<td>2,113<\/td>\n<td>454<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The interesting metrics here are the total lines of code in the data<br \/>\naccess layer, the total lines of code in the <em>largest single file<\/em> in<br \/>\nthe data access layer and the input tokens consumed while producing<br \/>\nthe change.<\/p>\n<p><\/p>\n<p>This chart shows four things. The first point is the baseline, step 0,<br \/>\nand then the same metrics are repeated <em>after<\/em> each refactoring step<br \/>\nhas been applied.<\/p>\n<ol>\n<li>The total lines of code in the data access layer as a<br \/>\nwhole. Initially, this is just the single file I started with. This<br \/>\nbecomes many files as refactorings are applied. By the end there<br \/>\nare 19 Rust files.<\/li>\n<li>The lines of code in the single largest file in the data access<br \/>\nlayer. This started as the entirety of the data layer in the single<br \/>\ninitial file.  By the end, the single largest file is a test<br \/>\nlibrary. Further refactoring passes could apply the same approach<br \/>\nto this.<\/li>\n<li>The total input tokens consumed by the sub-agent while applying the<br \/>\nrepresentative change.<\/li>\n<li>The total output tokens produced by the sub-agent while applying<br \/>\nthe representative change.<\/li>\n<\/ol>\n<h2 id=\"refactoring-reduces-token-consumption\">Refactoring reduces token consumption<\/h2>\n<p>The results are clear. Input tokens stay fairly flat until the largest<br \/>\nfile starts to fall, and then they drop before, in the words of<br \/>\nClaude, falling off a cliff.<\/p>\n<p>Between the base line and the final refactoring, input tokens for the<br \/>\nsame task reduced from <strong>159,564<\/strong> to <strong>27,360<\/strong>. A saving of<br \/>\n<strong>132,204<\/strong> tokens, or <strong>83%<\/strong>. And that saving is not a one-off. Every single<br \/>\nchange that touches the data access layer from this point forward now<br \/>\ncosts significantly less.<\/p>\n<p>How much of a saving? Assuming Sonnet 5 pricing at the time of writing<br \/>\nof $3\/MTok, 39.7 cents. Not a lot. Does it multiply? How will this<br \/>\nplay out across debugging? More complicated features? This is<br \/>\nrefactoring only one portion of the code base, can the whole code base<br \/>\nbe aggressively refactored to find savings everywhere? How much would<br \/>\nthose refactorings <em>cost?<\/em><\/p>\n<p>This saving is because the agent has to read less code. But it is not<br \/>\nbecause there is less code to read. The overall code in the data<br \/>\naccess layer as a whole has stayed fairly constant. Therefore to be<br \/>\nable to bank this saving, the agent must be able to successfully<br \/>\nidentify the smallest subset of files necessary to read. The results<br \/>\nmake it appear this was happening. Reading the Claude Code thinking<br \/>\noutput and file read summaries as the change was being applied also<br \/>\nindicates the sub-agent was successfully reading smaller and smaller<br \/>\nsections of code each time.<\/p>\n<p>In other words, randomly cutting the file into smaller files is<br \/>\nunlikely to help as much: even if each file were smaller, the agent<br \/>\nwould be forced to read through many files looking for the relevant<br \/>\ncode. While the step with the biggest effect happens at the end, the<br \/>\nprevious steps were refactorings to set up this saving. This was not<br \/>\nplanned. It was simply a result of how refactoring typically proceeds:<br \/>\nlocal file changes to extract duplication, before breaking down into<br \/>\nsmaller files once a repeating core emerges.<\/p>\n<p>The refactoring did not make the representative change smaller. The<br \/>\nnumber of tokens produced when writing code was largely unaffected:<br \/>\nthe output tokens do not move very much. Those tokens are five times<br \/>\nthe price of the input tokens. But, there are a lot less of them. Are<br \/>\nthere refactorings that could be applied to reduce output token<br \/>\nproduction? I need a more complex sample change to explore these<br \/>\nquestions. The noise of the non-deterministic code generation process<br \/>\nis hiding any variance caused by changes in the factoring of the code.<\/p>\n<h2 id=\"notes-on-the-process\">Notes on the process<\/h2>\n<p>Claude was not good at refactoring. If you read the prompt and the<br \/>\nrefactoring steps below, it\u2019s clear that the refactorings produced<br \/>\nwere directly in response to the prompt. Claude is unable to look at<br \/>\ncode, look at refactorings in general and work out which are suitable<br \/>\nto apply: a human needs to actively guide it. This marries with wider<br \/>\nexperience in this app. The development harness includes an explicit<br \/>\nrefactoring step. That refactoring step did not prompt Claude into<br \/>\nimproving this file. More anecdotally, Claude.ai was better than<br \/>\nClaude Code. I used both interfaces to create the refactoring<br \/>\nplan. Claude Code spotted extract function as the first<br \/>\nstep. Claude.ai went further and saw an entire client class to be<br \/>\nextracted.<\/p>\n<p>It was also bad at applying them. The mechanical act of refactoring<br \/>\nwas performed by writing Python scripts using grep and sed. These<br \/>\nscripts frequently got confused by indentation. Oh, the irony. In<br \/>\naddition, the single most valuable refactoring was missed in the first<br \/>\npass, and had to be re-applied as a follow-up step. This is why the<br \/>\nnumber of steps in the figure don\u2019t match the refactoring steps in the<br \/>\nappendix.<\/p>\n<p>It took about eight hours to complete the entire experiment. This was<br \/>\nmostly unattended. The only intervention was after six hours 40<br \/>\nminutes when it appeared to have finished, but had skipped that step<br \/>\nand needed to be redirected. This experiment was running on slow<br \/>\nhotel WiFi. I wondered if that contributed to time taken. But on<br \/>\ndeeper analysis of the code base, the cargo temporary build cache had<br \/>\nbecome very large. Test execution was suffering, significantly.<\/p>\n<h2 id=\"further-work-and-broader-implications\">Further work and broader implications<\/h2>\n<p>Unfortunately, it didn\u2019t occur to me to perform a count of the tokens<br \/>\nrequired to create and execute the refactoring plan until it was<br \/>\nalready complete. I\u2019ve looked at my aggregate consumption across the<br \/>\ntime window where I was doing this work, including designing and<br \/>\nrunning the experiment. I can\u2019t say how many tokens were required to<br \/>\nperform the refactoring. The upper bound is five million,<br \/>\nhowever. This includes creating the refactoring plan twice, the work<br \/>\nto design the experiment including the representative change, and<br \/>\nvarious other tasks. Future work should include a more accurate count<br \/>\nof tokens consumed to refactor.<\/p>\n<p>This is just one experiment, on a significant application that is<br \/>\nstill greenfield and built and maintained by a single developer. But,<br \/>\nI believe this is a potentially interesting first step. This effort<br \/>\nshows the value, in time and money of refactoring. As well as<br \/>\nmeasuring how expensive refactoring is. It would be interesting to<br \/>\nlook at more complex changes, at wider refactoring, refactoring<br \/>\ncontinuously, and even the relative value of different refactoring<br \/>\napproaches.<\/p>\n<p>This is just the beginning.<\/p>\n<h2 id=\"appendices\">Appendices<\/h2>\n<p><em>Note: These appendices include the prompts that I used, and the<br \/>\noutput that was returned. The only editing applied has been to remove<br \/>\nthe specific code changes to be made. These are included without<br \/>\nediting to show how the agents were directed. There are no hidden<br \/>\ntricks. As such, there is some language in here that might be<br \/>\nconfusing. The error is in the original.<\/em><\/p>\n<details>\n<summary>The representative change<\/summary>\n<p>This is the recorded prompt that was fed to each sub-agent, there was<br \/>\nno further context supplied other than the code base and accompanying<br \/>\narchitecture documentation. Every sub-agent was starting with exactly<br \/>\nthe same information.<\/p>\n<blockquote>\n<p>You are working in the Rust project at <code>~\/dev\/your-project-name<\/code>.<\/p>\n<p>Add a new <code>ItemWatchStore<\/code> public async trait to the Firestore layer, following existing patterns exactly. The trait must have three methods:<\/p>\n<ul>\n<li><code>async fn watch_item(&amp;self, item_id: &amp;str, user_id: &amp;str) -&gt; Result&lt;()&gt;<\/code><\/li>\n<li><code>async fn unwatch_item(&amp;self, item_id: &amp;str, user_id: &amp;str) -&gt; Result&lt;()&gt;<\/code><\/li>\n<li><code>async fn watched_items_for_user(&amp;self, user_id: &amp;str) -&gt; Result&lt;Vec&lt;String&gt;&gt;<\/code><\/li>\n<\/ul>\n<p>Watches are stored in a <code>item_watches<\/code> Firestore collection. Each document has fields: <code>itemId<\/code> (string), <code>userId<\/code> (string), <code>createdAt<\/code> (timestamp). There is no Rust struct for a watch record \u2014 the methods return <code>Vec&lt;String&gt;<\/code> (item ids).<\/p>\n<p>Implement the trait for both <code>FakeStore<\/code> (using an in-memory <code>Vec&lt;(String, String)&gt;<\/code> field added to <code>FakeStoreInner<\/code>) and <code>FirestoreStore<\/code> (using the same HTTP patterns used for other store impls in this file).<\/p>\n<p><strong>At the very end of your response<\/strong>, output exactly this JSON block (fill in real values):<\/p>\n<pre><code class=\"language-json\">{\n  \"files_read\": [\n    {\"path\": \"src\/firestore.rs\", \"chars\": 123456},\n    ...\n  ],\n  \"response_chars\": 7890\n}\n<\/code><\/pre>\n<p>Do NOT commit the change. Stop after writing the code.<\/p>\n<\/blockquote>\n<\/details>\n<details>\n<summary>Refactoring steps<\/summary>\n<p>This is the prompt that was used to create the refactoring plan.<\/p>\n<blockquote>\n<p>Following the strict definition that a refactoring is a provably<br \/>\ncorrectness preserving series of code edits, and using Martin<br \/>\nFowler\u2019s 2nd edition of Refactoring as the source, examine<br \/>\n@src\/firestore.rs. This is a 17K LoC Rust file. No file should be<br \/>\nthat long. It is almost certainly not using an internal language to<br \/>\nbuild and manage queries. Produce and describe, but don\u2019t execute, a<br \/>\nsequence of refactorings that would massively reduce the line count<br \/>\nof that file, without changing the interface at all.<\/p>\n<\/blockquote>\n<p>Following is the description of the refactorings applied, extracted<br \/>\nfrom the plan built and followed by Claude. The actual plan includes<br \/>\npredicted code changes. For each refactoring, the individual steps to<br \/>\nfollow were listed. Each of those steps was individually testable, and<br \/>\nwas individually tested. This is a stricter refactoring than most<br \/>\nhuman engineers would follow.<\/p>\n<p>The steps listed here don\u2019t line up directly with the measured changes<br \/>\nabove as Claude skipped the most valuable single refactoring<br \/>\n(splitting out the store into sub-files) on the first pass and had to<br \/>\ncomplete that afterwards as two additional steps.<\/p>\n<p><strong>Fowler ref:<\/strong> <em>Extract Class<\/em> (7.5); <em>Extract Function<\/em> (6.1) for<br \/>\neach primitive<\/p>\n<p><code>FirestoreStore<\/code> currently conflates two responsibilities:<\/p>\n<ul>\n<li><strong>Domain query orchestration<\/strong> \u2014 which query to run, which documents<br \/>\nto write, how to parse results into domain types<\/li>\n<li><strong>Firestore HTTP transport<\/strong> \u2014 auth headers, URL construction, JSON<br \/>\nencoding\/decoding of Firestore wire types,<br \/>\nretry-on-PRECONDITION_FAILED<\/li>\n<\/ul>\n<p>Fowler \u00a77.5 calls for extracting a new class when you can identify a<br \/>\ncoherent subset of a class\u2019s data and behaviour. The transport<br \/>\nresponsibility owns: <code>client: reqwest::Client<\/code>, <code>project_id: String<\/code>,<br \/>\n<code>MetadataAuth<\/code>, and <code>documents_url()<\/code> \/ <code>auth_header()<\/code>. Extract these<br \/>\ninto a new <code>FirestoreClient<\/code> struct.<\/p>\n<p><strong>Estimated savings: ~1,200 lines in <code>FirestoreStore<\/code> impls;<br \/>\n<code>FirestoreClient<\/code> adds ~120 lines net.<\/strong><\/p>\n<h4 id=\"step-2--extract-function-extractdocid-and-newlink-fowler-61\">Step 2 \u2014 Extract Function: <code>extract_doc_id<\/code> and <code>new_link<\/code> (Fowler \u00a76.1)<\/h4>\n<p><strong>Fowler ref:<\/strong> <em>Extract Function<\/em> (6.1)<\/p>\n<ul>\n<li>\n<p><strong><code>extract_doc_id<\/code><\/strong> \u2014 The expression<br \/>\n<code>doc.name.rsplit('\/').next()?.to_string()<\/code> appears verbatim at the<br \/>\nstart of all 20 <code>parse_*_document<\/code> functions. Extract it.<\/p>\n<\/li>\n<li>\n<p><strong><code>new_link<\/code><\/strong> \u2014 Building a <code>Link<\/code> struct with <code>metadata:<br \/>\nHashMap::new()<\/code> and <code>provenance: None<\/code> and a fresh UUID appears 62<br \/>\ntimes. Extract a factory function.<\/p>\n<\/li>\n<\/ul>\n<p><strong>Estimated savings: ~500 lines<\/strong> (62 \u00d7 ~10-line structs \u2192 62 \u00d7<br \/>\n~2-line calls; 20 parse functions each lose 1 line of boilerplate).<\/p>\n<p><strong>Fowler ref:<\/strong> <em>Extract Function<\/em> (6.1)<\/p>\n<p>Two sub-patterns recur inside the <code>FirestoreStore<\/code> trait impls after running a link query:<\/p>\n<ul>\n<li>\n<p><strong>Pattern A \u2014 collect all link documents from query rows (~15<br \/>\nsites).<\/strong><\/p>\n<\/li>\n<li>\n<p><strong>Pattern B \u2014 query links and return exactly one target ID, error if<br \/>\nmissing (~8 sites):<\/strong><\/p>\n<\/li>\n<\/ul>\n<p><strong>Estimated savings: ~200 lines.<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Extract Function<\/em> (6.1)<\/p>\n<p>Inside the FakeStore impls, ~15 methods repeat variations of<br \/>\n<code>inner.links.iter()...<\/code>.<\/p>\n<p>Extract two methods on <code>FakeStoreInner<\/code>. The 15 callsites then become<br \/>\nsingle-line. Methods that additionally filter by a second predicate<br \/>\n(e.g. also checking <code>to_kind<\/code>) chain <code>.into_iter().filter(\u2026)<\/code> on the<br \/>\nresult of the helper.<\/p>\n<p><strong>Estimated savings: ~120 lines.<\/strong><\/p>\n<h4 id=\"step-5--replace-inline-code-with-function-call--4-firestore-value-constructors\">Step 5 \u2014 Replace Inline Code with Function Call \u00d7 4: Firestore value constructors<\/h4>\n<p><strong>Fowler ref:<\/strong> <em>Replace Inline Code with Function Call<\/em> (8.5)<\/p>\n<p>Add four private free functions (file-level, not methods) before the<br \/>\ncodec block. Replace all 128+ <code>json!({\"stringValue\": \u2026})<\/code> \/<br \/>\n<code>json!({\"timestampValue\": \u2026})<\/code> etc. inline expressions with calls to<br \/>\nthese functions. Each multi-word json macro call becomes a single<br \/>\nshort call.<\/p>\n<p><strong>Estimated savings: ~80 lines (mostly from multi-line json macros collapsing to one-liners).<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Extract Class<\/em> (7.3)<\/p>\n<p>The ~20 encoder functions all follow this shape:<\/p>\n<pre><code class=\"language-rust\">let mut fields = serde_json::Map::new();\nfields.insert(\"foo\".to_string(), str_val(&amp;x.foo));\nfields.insert(\"bar\".to_string(), ts_val(x.bar));\njson!({\"name\": path, \"fields\": fields})\n<\/code><\/pre>\n<p>Extract a small builder. Rewrite each encoder function to use the<br \/>\nbuilder. A ~40-line encoder shrinks to ~12 lines.<\/p>\n<p><strong>Estimated savings: ~500\u2013600 lines across the 20 encoder functions.<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Convert <code>src\/firestore.rs<\/code> to a module directory: rename to<br \/>\n<code>src\/firestore\/mod.rs<\/code>. Then create <code>src\/firestore\/queries.rs<\/code> and<br \/>\nmove all 32 <code>LinkQuery<\/code> constants and the<br \/>\n<code>LinkQuery<\/code>\/<code>EqFilter<\/code>\/<code>EqValue<\/code>\/<code>Ordering<\/code>\/<code>Direction<\/code> type<br \/>\ndefinitions into it. Add <code>pub(super) use queries::*;<\/code> in <code>mod.rs<\/code>.<\/p>\n<p>No behaviour changes; all callsites already reference names that were<br \/>\nin scope via the flat file.<\/p>\n<p><strong>Reduces <code>mod.rs<\/code> by ~800 lines.<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Move all 17 <code>pub trait<\/code> definitions (and their associated error types)<br \/>\nto <code>src\/firestore\/traits.rs<\/code>. Re-export them from <code>mod.rs<\/code> with <code>pub<br \/>\nuse traits::*;<\/code>.<\/p>\n<p><strong>Reduces <code>mod.rs<\/code> by ~1,900 lines. Produces a ~1,900-line <code>traits.rs<\/code><br \/>\nthat needs further decomposition.<\/strong><\/p>\n<h4 id=\"step-9--move-function-split-traitsrs-into-a-traits-module-directory\">Step 9 \u2014 Move Function: split <code>traits.rs<\/code> into a <code>traits\/<\/code> module directory<\/h4>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Convert <code>src\/firestore\/traits.rs<\/code> to a module directory by grouping the 17 traits into four domain-aligned files:<\/p>\n<table>\n<thead>\n<tr>\n<th>File<\/th>\n<th>Traits<\/th>\n<th>Approx lines<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>traits\/planning.rs<\/code><\/td>\n<td><code>ConcentrationStore<\/code>, <code>GoalStore<\/code>, <code>ItemStore<\/code>, <code>NoteStore<\/code>, <code>PursuitStore<\/code>, <code>FocusPassStore<\/code><\/td>\n<td>~650<\/td>\n<\/tr>\n<tr>\n<td><code>traits\/content.rs<\/code><\/td>\n<td><code>CaptureStore<\/code>, <code>TagStore<\/code>, <code>UrlReferenceStore<\/code>, <code>DocumentStore<\/code>, <code>PaperStore<\/code><\/td>\n<td>~550<\/td>\n<\/tr>\n<tr>\n<td><code>traits\/people.rs<\/code><\/td>\n<td><code>ThoughtworkerStore<\/code>, <code>ExternalContactStore<\/code>, <code>CompanyStore<\/code><\/td>\n<td>~300<\/td>\n<\/tr>\n<tr>\n<td><code>traits\/system.rs<\/code><\/td>\n<td><code>SessionState<\/code>, <code>LinkStore<\/code>, <code>SuggestionStore<\/code>, <code>SuggestionVetoStore<\/code>, <code>OAuthTokenStore<\/code>, <code>MigrationLedger<\/code>, <code>EmbeddingStore<\/code>, <code>RuntimeConfigStore<\/code>, <code>SalesforceSyncStateStore<\/code><\/td>\n<td>~400<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>traits\/mod.rs<\/code> becomes a pure re-export file (~20 lines). Associated<br \/>\nerror types (<code>FocusPassError<\/code>, <code>SuggestionDecisionError<\/code>, etc.) move<br \/>\nwith the trait that produces them.<\/p>\n<p><strong>No trait definition changes, no callsite changes \u2014 only<br \/>\nrelocation. Each resulting file is 300\u2013650 lines.<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Move all document encoder\/decoder functions (<code>*_document<\/code>,<br \/>\n<code>parse_*_document<\/code>, <code>kind_str<\/code>, <code>parse_kind<\/code>, <code>parse_capture_source<\/code>,<br \/>\n<code>parse_outcome<\/code>, etc.) plus <code>FieldsBuilder<\/code> and the value constructors<br \/>\nfrom Steps 5 and 6 into <code>src\/firestore\/codec.rs<\/code>. Make them<br \/>\n<code>pub(super)<\/code>.<\/p>\n<p>After Step 6 this module will be ~400\u2013500 lines rather than ~1,200.<\/p>\n<p><strong>Reduces <code>mod.rs<\/code> by ~500 lines (post-Step-6).<\/strong><\/p>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Move <code>FakeStore<\/code>, <code>FakeStoreInner<\/code>, and all 18 trait impl blocks for<br \/>\n<code>FakeStore<\/code> into <code>src\/firestore\/fake_store.rs<\/code>. Re-export <code>FakeStore<\/code><br \/>\nfrom <code>mod.rs<\/code> with <code>pub use fake_store::FakeStore;<\/code>.<\/p>\n<p><code>FakeStoreInner<\/code> and helper methods stay private to the module.<\/p>\n<p><strong>Reduces <code>mod.rs<\/code> by ~4,700 lines.<\/strong><\/p>\n<h4 id=\"step-12--move-function-split-firestorestore-impls-into-per-trait-files-under-srcfirestorestore\">Step 12 \u2014 Move Function: split <code>FirestoreStore<\/code> impls into per-trait files under <code>src\/firestore\/store\/<\/code><\/h4>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>Create <code>src\/firestore\/store\/mod.rs<\/code> with <code>FirestoreStore<\/code> struct<br \/>\ndefinition, <code>impl FirestoreStore<\/code> (constructor + <code>FirestoreClient<\/code><br \/>\nfrom Step 1), and <code>MetadataAuth<\/code>.<\/p>\n<p>Then create one file per logical domain grouping.<\/p>\n<p>Each file contains only <code>use super::*;<\/code> (or explicit imports) and the<br \/>\ntrait impl block(s). No type definitions, no helpers. Helpers used by<br \/>\nmultiple impl blocks stay in <code>store\/mod.rs<\/code>.<\/p>\n<p><strong>Reduces what would be a ~10,000-line file into ten files of 120\u2013650<br \/>\nlines each. <code>mod.rs<\/code> becomes a ~100-line re-export manifest.<\/strong><\/p>\n<h4 id=\"step-13--move-function-co-locate-tests-with-their-modules\">Step 13 \u2014 Move Function: co-locate tests with their modules<\/h4>\n<p><strong>Fowler ref:<\/strong> <em>Move Function<\/em> (8.1)<\/p>\n<p>The existing <code>#[cfg(test)]<\/code> modules test specific domain areas and<br \/>\nbelong with the modules created in Step 12 rather than in a single<br \/>\n<code>tests.rs<\/code>.Each test module moves inside a <code>#[cfg(test)] mod tests { \u2026<br \/>\n}<\/code> block at the bottom of the target file, with <code>use super::*;<\/code> to<br \/>\naccess the module\u2019s internals. No test is changed, only relocated.<\/p>\n<p>Any shared test fixtures (<code>FakeStore::new<\/code>, helper builders) that are<br \/>\nalready in <code>fake_store.rs<\/code> are accessible via the existing <code>use<br \/>\nsuper::fake_store::FakeStore<\/code> import chain.<\/p>\n<p><strong>Reduces <code>mod.rs<\/code> by ~2,000 lines; each target file gains 200\u2013700<br \/>\nlines of tests that are directly adjacent to the code they exercise.<\/strong><\/p>\n<\/details>\n<\/div>\n<p><a href=\"https:\/\/martinfowler.com\/articles\/exploring-gen-ai\/refactoring-economic-benefit.html?utm_source=tldrdev\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>As part of getting to grips with the new world of agentic engineering, I built an application to support my work. It\u2019s a sophisticated app: high-quality web UI with dynamic refresh and look-up, modals and auto-save, integrations to external systems, machine learning and text analysis, background jobs, and a proper environment setup with fully automated [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":22967,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-22966","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22966","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=22966"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22966\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/22967"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=22966"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=22966"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=22966"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}